From 26bc1d52db93640ca0e7c08cd8f821b65bcccd1c Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Wed, 28 Jan 2026 16:53:34 +0900 Subject: [PATCH 1/6] docs(readme): add feature list organize feature list in README before implementation according to precourse requirements --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d7e8aee..b679c3bf 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ -# java-baseball-precourse \ No newline at end of file +# [ ⚾ 숫자 야구 게임 ⚾ ] + +## 📑 기능 목록 + +### 1. 게임 시작(초기화) 시 난수 생성 +* 게임 시작 시 1에서 9까지 서로 다른 임의의 수 3개를 선택하여 컴퓨터의 숫자를 생성한다. + +### 2. 사용자 입력 및 유효성 검사 +* `숫자를 입력해주세요 : ` 문구를 출력하고 사용자로부터 3자리의 숫자를 입력받는다. +* 사용자가 유효하지 않은 값을 입력할 경우 `[ERROR]` 메시지를 출력한다. + * (검증 기준: 숫자가 아닌 값, 3자리가 아닌 경우, 1~9 범위를 벗어난 수) + * 사용자의 경우 3자리 각 숫자가 서로 달라야 한다는 조건은 없다. + +### 3. 볼/스트라이크 판정 및 결과 출력 +* 컴퓨터의 수와 플레이어의 수를 비교하여 결과를 판정한다. + * 같은 수가 같은 자리에 있으면 `스트라이크` + * 같은 수가 다른 자리에 있으면 `볼` + * 같은 수가 전혀 없으면 `낫싱` +* 판정 결과를 `1스트라이크 1볼`, `낫싱`, `3스트라이크`와 같은 형식으로 출력. + +### 4. 게임 종료 및 재시작 +* 3스트라이크가 되면 `3개의 숫자를 모두 맞히셨습니다! 게임 끝` 메시지를 출력하며 게임을 종료한다. +* 게임 종료 후 `게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.` 문구를 출력한다. +* 입력값에 따라 게임을 재시작(1)하거나 완전히 종료(2)한다. \ No newline at end of file From a3935c343099d38be71dcd477990705e9fb9baca Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Wed, 28 Jan 2026 18:10:02 +0900 Subject: [PATCH 2/6] feat(domain): generate 3-digit computer numbers implement number generator with distinct digits in range 1-9 and add unit tests for generation rules Refs: README feature #1 (random number generation) --- src/main/java/baseball/Application.java | 19 +++++++ .../baseball/controller/GameController.java | 23 ++++++++ .../java/baseball/domain/ComputerNumbers.java | 22 ++++++++ .../baseball/domain/NumbersGenerator.java | 53 +++++++++++++++++++ .../baseball/domain/NumbersGeneratorTest.java | 53 +++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 src/main/java/baseball/Application.java create mode 100644 src/main/java/baseball/controller/GameController.java create mode 100644 src/main/java/baseball/domain/ComputerNumbers.java create mode 100644 src/main/java/baseball/domain/NumbersGenerator.java create mode 100644 src/test/java/baseball/domain/NumbersGeneratorTest.java diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java new file mode 100644 index 00000000..495bc38b --- /dev/null +++ b/src/main/java/baseball/Application.java @@ -0,0 +1,19 @@ +package baseball; + +import baseball.controller.GameController; +import baseball.domain.NumbersGenerator; + +// 프로그램 실행 시작점 +public class Application { + public static void main(String[] args) { + // 컴퓨터 숫자 생성을 담당하는 생성기 준비 + NumbersGenerator generator = new NumbersGenerator(); + + // 게임 실행 흐름을 제어하는 컨트롤러 생성 + GameController gameController = new GameController(generator); + + // 숫자 야구 게임 실행 + gameController.run(); + } +} + diff --git a/src/main/java/baseball/controller/GameController.java b/src/main/java/baseball/controller/GameController.java new file mode 100644 index 00000000..bbb90c35 --- /dev/null +++ b/src/main/java/baseball/controller/GameController.java @@ -0,0 +1,23 @@ +package baseball.controller; + +import baseball.domain.ComputerNumbers; +import baseball.domain.NumbersGenerator; + +// 게임의 전체 실행 흐름을 제어 +public class GameController { + + // 컴퓨터 숫자 생성을 담당하는 생성기 + private final NumbersGenerator generator; + + // 숫자 생성기 주입 + public GameController(NumbersGenerator generator) { + this.generator = generator; + } + + // 게임 실행 + public void run() { + // 임의 숫자 생성 + ComputerNumbers computerNumbers = generator.generate(); + System.out.println(computerNumbers); + } +} diff --git a/src/main/java/baseball/domain/ComputerNumbers.java b/src/main/java/baseball/domain/ComputerNumbers.java new file mode 100644 index 00000000..71432e79 --- /dev/null +++ b/src/main/java/baseball/domain/ComputerNumbers.java @@ -0,0 +1,22 @@ +package baseball.domain; + +// 컴퓨터가 뽑은 임의의 수를 하나의 객체로 관리 +public class ComputerNumbers { + private final int[] digits; + + public ComputerNumbers(int[] digits) { + this.digits = digits; + } + + + // 특정 위치의 숫자 조회 + public int digitAt(int index) { + return digits[index]; + } + + // 디버깅 용 + @Override + public String toString() { + return "digits: " + digits[0] + digits[1] + digits[2]; + } +} diff --git a/src/main/java/baseball/domain/NumbersGenerator.java b/src/main/java/baseball/domain/NumbersGenerator.java new file mode 100644 index 00000000..d8168636 --- /dev/null +++ b/src/main/java/baseball/domain/NumbersGenerator.java @@ -0,0 +1,53 @@ +package baseball.domain; + +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +/** + * 컴퓨터가 사용할 숫자 3개를 생성하는 역할을 담당 + * 책임: + * - 1~9 범위의 숫자를 사용 + * - 서로 다른 숫자 3개를 생성 + * - 생성된 결과를 ComputerNumbers 객체로 반환 + * 숫자의 유효성(범위, 중복 없음)은 생성 과정에서 보장 (단위 테스트 진행) + */ +public class NumbersGenerator { + // 생성할 숫자의 개수 + private static final int SIZE = 3; + + // 생성 가능한 숫자의 최댓값 (1 ~ 9) + private static final int MAX = 9; + + // 난수 생성을 위한 Random 객체 + private final Random random = new Random(); + + // 1~9 범위의 서로 다른 숫자 3개를 생성하여 ComputerNumbers로 반환 + public ComputerNumbers generate() { + Set numbers = new HashSet<>(); + + while (numbers.size() < SIZE) { + numbers.add(randomNumber()); + } + + return new ComputerNumbers(toArray(numbers)); + } + + // 1~9 범위의 난수 하나를 생성 + private int randomNumber() { + return random.nextInt(MAX) + 1; // 1 ~ 9 + } + + // 생성된 숫자 Set를 배열 형태로 변환 + private int[] toArray(Set numbers) { + int[] result = new int[SIZE]; + int index = 0; + + for (int number : numbers) { + result[index] = number; + index++; + } + + return result; + } +} diff --git a/src/test/java/baseball/domain/NumbersGeneratorTest.java b/src/test/java/baseball/domain/NumbersGeneratorTest.java new file mode 100644 index 00000000..2b3bc9fd --- /dev/null +++ b/src/test/java/baseball/domain/NumbersGeneratorTest.java @@ -0,0 +1,53 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class NumbersGeneratorTest { + + @DisplayName("컴퓨터 숫자는 3자리로 생성") + @Test + void generate_creates_three_digits() { + NumbersGenerator generator = new NumbersGenerator(); + + ComputerNumbers numbers = generator.generate(); + + assertThat(numbers).isNotNull(); + // digitAt이 0~2까지 접근 가능하면 3자리 보장 + assertThat(numbers.digitAt(0)).isNotNull(); + assertThat(numbers.digitAt(1)).isNotNull(); + assertThat(numbers.digitAt(2)).isNotNull(); + } + + @DisplayName("컴퓨터 숫자의 각 자리는 1~9 범위") + @Test + void generate_digits_are_between_1_and_9() { + NumbersGenerator generator = new NumbersGenerator(); + + ComputerNumbers numbers = generator.generate(); + + assertThat(numbers.digitAt(0)).isBetween(1, 9); + assertThat(numbers.digitAt(1)).isBetween(1, 9); + assertThat(numbers.digitAt(2)).isBetween(1, 9); + } + + @DisplayName("컴퓨터 숫자는 서로 다른 3개의 숫자") + @Test + void generate_digits_are_distinct() { + NumbersGenerator generator = new NumbersGenerator(); + + ComputerNumbers numbers = generator.generate(); + + Set set = new HashSet<>(); + set.add(numbers.digitAt(0)); + set.add(numbers.digitAt(1)); + set.add(numbers.digitAt(2)); + + assertThat(set).hasSize(3); + } +} From 7606f17d5a4088998ae347eb48e9691e47636d02 Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Fri, 30 Jan 2026 16:41:07 +0900 Subject: [PATCH 3/6] feat(domain): parse and validate user input guess add Guess domain to parse and validate 3-digit input and retry until valid input is provided Refs: README feature #2 --- src/main/java/baseball/Application.java | 19 ----- .../baseball/BaseballGameApplication.java | 18 +++++ .../baseball/controller/GameController.java | 36 ++++++--- .../java/baseball/domain/ComputerNumbers.java | 2 +- src/main/java/baseball/domain/Guess.java | 78 +++++++++++++++++++ src/main/java/baseball/domain/GuessError.java | 19 +++++ src/main/java/baseball/view/InputView.java | 16 ++++ src/test/java/baseball/domain/GuessTest.java | 77 ++++++++++++++++++ 8 files changed, 236 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/baseball/Application.java create mode 100644 src/main/java/baseball/BaseballGameApplication.java create mode 100644 src/main/java/baseball/domain/Guess.java create mode 100644 src/main/java/baseball/domain/GuessError.java create mode 100644 src/main/java/baseball/view/InputView.java create mode 100644 src/test/java/baseball/domain/GuessTest.java diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java deleted file mode 100644 index 495bc38b..00000000 --- a/src/main/java/baseball/Application.java +++ /dev/null @@ -1,19 +0,0 @@ -package baseball; - -import baseball.controller.GameController; -import baseball.domain.NumbersGenerator; - -// 프로그램 실행 시작점 -public class Application { - public static void main(String[] args) { - // 컴퓨터 숫자 생성을 담당하는 생성기 준비 - NumbersGenerator generator = new NumbersGenerator(); - - // 게임 실행 흐름을 제어하는 컨트롤러 생성 - GameController gameController = new GameController(generator); - - // 숫자 야구 게임 실행 - gameController.run(); - } -} - diff --git a/src/main/java/baseball/BaseballGameApplication.java b/src/main/java/baseball/BaseballGameApplication.java new file mode 100644 index 00000000..2ca86afb --- /dev/null +++ b/src/main/java/baseball/BaseballGameApplication.java @@ -0,0 +1,18 @@ +package baseball; + +import baseball.controller.GameController; +import baseball.domain.NumbersGenerator; +import baseball.view.InputView; + +// 숫자 야구 게임 시작점 +public class BaseballGameApplication { + + public static void main(String[] args) { + // 게임 실행 흐름을 제어하는 컨트롤러 구성 + GameController gameController = new GameController(new NumbersGenerator(), new InputView()); + + // 숫자 야구 게임 실행 + gameController.run(); + + } +} \ No newline at end of file diff --git a/src/main/java/baseball/controller/GameController.java b/src/main/java/baseball/controller/GameController.java index bbb90c35..b65eb850 100644 --- a/src/main/java/baseball/controller/GameController.java +++ b/src/main/java/baseball/controller/GameController.java @@ -1,23 +1,41 @@ package baseball.controller; import baseball.domain.ComputerNumbers; +import baseball.domain.Guess; import baseball.domain.NumbersGenerator; +import baseball.view.InputView; -// 게임의 전체 실행 흐름을 제어 -public class GameController { +import java.io.IOException; - // 컴퓨터 숫자 생성을 담당하는 생성기 +public class GameController { private final NumbersGenerator generator; + private final InputView inputView; - // 숫자 생성기 주입 - public GameController(NumbersGenerator generator) { - this.generator = generator; + public GameController(NumbersGenerator generator, InputView inputView) { + this.generator = generator; // 숫자 생성기 + this.inputView = inputView; // 유저 입력받는 객체 } - // 게임 실행 public void run() { - // 임의 숫자 생성 + // 컴퓨터가 생성한 숫자 ComputerNumbers computerNumbers = generator.generate(); System.out.println(computerNumbers); + + // 유저가 입력한 숫자 + Guess guess = readGuessUntilValid(); + System.out.println(guess); + } + + // 올바르게 입력할때 까지 재시도 - Guess는 사용자가 입력할 예측값 + private Guess readGuessUntilValid() { + while (true) { + try { + return Guess.from(inputView.readGuess()); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } -} +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/ComputerNumbers.java b/src/main/java/baseball/domain/ComputerNumbers.java index 71432e79..8f9739aa 100644 --- a/src/main/java/baseball/domain/ComputerNumbers.java +++ b/src/main/java/baseball/domain/ComputerNumbers.java @@ -17,6 +17,6 @@ public int digitAt(int index) { // 디버깅 용 @Override public String toString() { - return "digits: " + digits[0] + digits[1] + digits[2]; + return "컴퓨터가 생성한 숫자: " + digits[0] + digits[1] + digits[2]; } } diff --git a/src/main/java/baseball/domain/Guess.java b/src/main/java/baseball/domain/Guess.java new file mode 100644 index 00000000..4febfc98 --- /dev/null +++ b/src/main/java/baseball/domain/Guess.java @@ -0,0 +1,78 @@ +package baseball.domain; + +import java.util.Objects; + +// 사용자가 입력한 3자리 숫자를 도메인 객체로 표현 +public class Guess { + private static final int SIZE = 3; + private static final int MIN = 1; + private static final int MAX = 9; + + private final int[] digits; + + // 숫자 3자리를 내부 상태로 보관 + private Guess(int[] digits) { + this.digits = digits; + } + + // 사용자 입력을 순서대로 검증한 뒤 Guess로 변환 + public static Guess from(String input) { + String value = validateNotNullAndTrim(input); + validateLength(value); + validateDigits(value); + int[] digits = toDigits(value); + validateRange(digits); + return new Guess(digits); + } + + // 지정한 위치의 숫자 반환 + public int digitAt(int index) { + return digits[index]; + } + + // null 여부 확인 후 앞뒤 공백 제거 + private static String validateNotNullAndTrim(String input) { + Objects.requireNonNull(input, GuessError.NULL_INPUT.message()); + return input.trim(); + } + + // 입력 문자열의 길이가 정확히 3인지 검증 + private static void validateLength(String value) { + if (value.length() != SIZE) { + throw new IllegalArgumentException(GuessError.INVALID_LENGTH.message()); + } + } + + // 모든 문자가 숫자인지 검증 + private static void validateDigits(String value) { + for (int i = 0; i < SIZE; i++) { + if (!Character.isDigit(value.charAt(i))) { + throw new IllegalArgumentException(GuessError.NON_DIGIT.message()); + } + } + } + + // 각 자리 숫자가 1~9 범위인지 검증 + private static void validateRange(int[] digits) { + for (int digit : digits) { + if (digit < MIN || digit > MAX) { + throw new IllegalArgumentException(GuessError.OUT_OF_RANGE.message()); + } + } + } + + // 문자열의 각 문자를 정수 배열로 변환 + private static int[] toDigits(String value) { + int[] result = new int[SIZE]; + for (int i = 0; i < SIZE; i++) { + result[i] = value.charAt(i) - '0'; + } + return result; + } + + // 디버깅 용 + @Override + public String toString() { + return "유저가 입력한 숫자: " + digits[0] + digits[1] + digits[2]; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/GuessError.java b/src/main/java/baseball/domain/GuessError.java new file mode 100644 index 00000000..4090d7a8 --- /dev/null +++ b/src/main/java/baseball/domain/GuessError.java @@ -0,0 +1,19 @@ +package baseball.domain; + +// Guess 생성 과정에서 발생할 수 있는 에러 유형 +public enum GuessError { + NULL_INPUT("[ERROR] 입력값이 null입니다."), + INVALID_LENGTH("[ERROR] 3자리 숫자를 입력해야 합니다."), + NON_DIGIT("[ERROR] 숫자만 입력할 수 있습니다."), + OUT_OF_RANGE("[ERROR] 1~9 범위의 숫자만 입력할 수 있습니다."); + + private final String message; + + GuessError(String message) { + this.message = message; + } + + public String message() { + return message; + } +} diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 00000000..dc818f60 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,16 @@ +package baseball.view; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +// 사용자 입력 처리(UI) +public class InputView { + private final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + // 숫자 입력 안내 후 문자열 입력 수집 + public String readGuess() throws IOException { + System.out.print("숫자를 입력해주세요 : "); + return br.readLine(); + } +} diff --git a/src/test/java/baseball/domain/GuessTest.java b/src/test/java/baseball/domain/GuessTest.java new file mode 100644 index 00000000..6ef32473 --- /dev/null +++ b/src/test/java/baseball/domain/GuessTest.java @@ -0,0 +1,77 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GuessTest { + + @DisplayName("3자리 숫자 문자열을 입력하면 각 자리가 올바르게 저장된다") + @Test + void from_valid_input_creates_guess() { + Guess guess = Guess.from("123"); + + assertThat(guess.digitAt(0)).isEqualTo(1); + assertThat(guess.digitAt(1)).isEqualTo(2); + assertThat(guess.digitAt(2)).isEqualTo(3); + } + + @DisplayName("사용자 입력은 중복 숫자를 허용한다") + @Test + void from_allows_duplicate_digits() { + Guess guess = Guess.from("111"); + + assertThat(guess.digitAt(0)).isEqualTo(1); + assertThat(guess.digitAt(1)).isEqualTo(1); + assertThat(guess.digitAt(2)).isEqualTo(1); + } + + @DisplayName("앞뒤 공백은 제거한 뒤 검증한다") + @Test + void from_trims_input() { + Guess guess = Guess.from(" 789 "); + + assertThat(guess.digitAt(0)).isEqualTo(7); + assertThat(guess.digitAt(1)).isEqualTo(8); + assertThat(guess.digitAt(2)).isEqualTo(9); + } + + @DisplayName("입력값이 null이면 예외가 발생한다") + @Test + void from_null_throws() { + assertThatThrownBy(() -> Guess.from(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageStartingWith("[ERROR]"); + } + + @DisplayName("3자리가 아니면 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", "1", "12", "1234", " 12", "12 ", "9999"}) + void from_invalid_length_throws(String input) { + assertThatThrownBy(() -> Guess.from(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("[ERROR]"); + } + + @DisplayName("숫자가 아닌 문자가 포함되면 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"12a", "a23", "1 3", "1,2", "+++"}) + void from_non_digit_throws(String input) { + assertThatThrownBy(() -> Guess.from(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("[ERROR]"); + } + + @DisplayName("0 또는 10 이상이 포함되면 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"023", "120", "900"}) + void from_out_of_range_throws(String input) { + assertThatThrownBy(() -> Guess.from(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("[ERROR]"); + } +} From e6c67d7e455139b52ebbb34fd574dddf6b67a8ea Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Fri, 30 Jan 2026 18:05:11 +0900 Subject: [PATCH 4/6] feat(domain): judge strikes and balls add Judge and Result to compare computer numbers and user guess and print strike/ball/nothing result until 3 strikes Refs: README feature #3 --- .../baseball/BaseballGameApplication.java | 4 +- .../baseball/controller/GameController.java | 27 ++++++--- .../java/baseball/domain/ComputerNumbers.java | 13 +++- src/main/java/baseball/domain/Judge.java | 45 ++++++++++++++ src/main/java/baseball/domain/Result.java | 33 ++++++++++ src/main/java/baseball/view/OutputView.java | 37 ++++++++++++ src/test/java/baseball/domain/JudgeTest.java | 60 +++++++++++++++++++ 7 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 src/main/java/baseball/domain/Judge.java create mode 100644 src/main/java/baseball/domain/Result.java create mode 100644 src/main/java/baseball/view/OutputView.java create mode 100644 src/test/java/baseball/domain/JudgeTest.java diff --git a/src/main/java/baseball/BaseballGameApplication.java b/src/main/java/baseball/BaseballGameApplication.java index 2ca86afb..6af0e8ac 100644 --- a/src/main/java/baseball/BaseballGameApplication.java +++ b/src/main/java/baseball/BaseballGameApplication.java @@ -1,15 +1,17 @@ package baseball; import baseball.controller.GameController; +import baseball.domain.Judge; import baseball.domain.NumbersGenerator; import baseball.view.InputView; +import baseball.view.OutputView; // 숫자 야구 게임 시작점 public class BaseballGameApplication { public static void main(String[] args) { // 게임 실행 흐름을 제어하는 컨트롤러 구성 - GameController gameController = new GameController(new NumbersGenerator(), new InputView()); + GameController gameController = new GameController(new NumbersGenerator(), new InputView(), new OutputView(), new Judge()); // 숫자 야구 게임 실행 gameController.run(); diff --git a/src/main/java/baseball/controller/GameController.java b/src/main/java/baseball/controller/GameController.java index b65eb850..d4da5c4c 100644 --- a/src/main/java/baseball/controller/GameController.java +++ b/src/main/java/baseball/controller/GameController.java @@ -1,19 +1,22 @@ package baseball.controller; -import baseball.domain.ComputerNumbers; -import baseball.domain.Guess; -import baseball.domain.NumbersGenerator; +import baseball.domain.*; import baseball.view.InputView; +import baseball.view.OutputView; import java.io.IOException; public class GameController { private final NumbersGenerator generator; private final InputView inputView; + private final OutputView outputView; + private final Judge judge; - public GameController(NumbersGenerator generator, InputView inputView) { - this.generator = generator; // 숫자 생성기 - this.inputView = inputView; // 유저 입력받는 객체 + public GameController(NumbersGenerator generator, InputView inputView, OutputView outputView, Judge judge) { + this.generator = generator; // 컴퓨터 숫자 생성기 + this.inputView = inputView; // 사용자 입력 담당 뷰 + this.outputView = outputView; // 결과 출력 담당 뷰 + this.judge = judge; // 스트라이크, 볼 판단 } public void run() { @@ -21,9 +24,15 @@ public void run() { ComputerNumbers computerNumbers = generator.generate(); System.out.println(computerNumbers); - // 유저가 입력한 숫자 - Guess guess = readGuessUntilValid(); - System.out.println(guess); + while (true) { + // 유저가 입력한 숫자 + Guess guess = readGuessUntilValid(); + System.out.println(guess); + Result result = judge.judge(computerNumbers, guess); + outputView.printResult(result); + + if (result.isThreeStrikes()) return; // 3스트라이크면 종료 + } } // 올바르게 입력할때 까지 재시도 - Guess는 사용자가 입력할 예측값 diff --git a/src/main/java/baseball/domain/ComputerNumbers.java b/src/main/java/baseball/domain/ComputerNumbers.java index 8f9739aa..60eff9e8 100644 --- a/src/main/java/baseball/domain/ComputerNumbers.java +++ b/src/main/java/baseball/domain/ComputerNumbers.java @@ -8,15 +8,24 @@ public ComputerNumbers(int[] digits) { this.digits = digits; } - // 특정 위치의 숫자 조회 public int digitAt(int index) { return digits[index]; } + // 숫자가 포함되어 있는지 여부 반환 + public boolean contains(int value) { + for (int digit : digits) { + if (digit == value) { + return true; + } + } + return false; + } + // 디버깅 용 @Override public String toString() { return "컴퓨터가 생성한 숫자: " + digits[0] + digits[1] + digits[2]; } -} +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/Judge.java b/src/main/java/baseball/domain/Judge.java new file mode 100644 index 00000000..6a4b7838 --- /dev/null +++ b/src/main/java/baseball/domain/Judge.java @@ -0,0 +1,45 @@ +package baseball.domain; + +// 컴퓨터 숫자와 사용자 입력을 비교해 결과를 계산 +public class Judge { + private static final int SIZE = 3; + + // 컴퓨터 숫자와 사용자 입력을 비교해 스트라이크, 볼 개수 계산 + public Result judge(ComputerNumbers answer, Guess guess) { + int strikes = countStrikes(answer, guess); + int balls = countBalls(answer, guess); + return new Result(strikes, balls); + } + + // 같은 자리에서 같은 숫자인 경우 스트라이크 개수 계산 + private int countStrikes(ComputerNumbers answer, Guess guess) { + int count = 0; + + for (int index = 0; index < SIZE; index++) { + if (answer.digitAt(index) == guess.digitAt(index)) { + count++; + } + } + + return count; + } + + // 자리는 다르지만 포함된 숫자인 경우 볼 개수 계산 + private int countBalls(ComputerNumbers answer, Guess guess) { + int count = 0; + + for (int index = 0; index < SIZE; index++) { + int value = guess.digitAt(index); + + if (answer.digitAt(index) == value) { + continue; + } + + if (answer.contains(value)) { + count++; + } + } + + return count; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/Result.java b/src/main/java/baseball/domain/Result.java new file mode 100644 index 00000000..6cc96262 --- /dev/null +++ b/src/main/java/baseball/domain/Result.java @@ -0,0 +1,33 @@ +package baseball.domain; + +// 스트라이크, 볼 개수를 보관하는 값 객체 +public class Result { + private final int strikes; + private final int balls; + + // 스트라이크, 볼 개수를 가진 결과 생성 + public Result(int strikes, int balls) { + this.strikes = strikes; + this.balls = balls; + } + + // 스트라이크 개수 반환 + public int strikes() { + return strikes; + } + + // 볼 개수 반환 + public int balls() { + return balls; + } + + // 스트라이크와 볼이 모두 없는지 여부 반환 + public boolean isNothing() { + return strikes == 0 && balls == 0; + } + + // 3스트라이크인지 여부 반환 + public boolean isThreeStrikes() { + return strikes == 3; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 00000000..9fec3830 --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,37 @@ +package baseball.view; + +import baseball.domain.Result; + +// 판정 결과를 콘솔에 출력하는 뷰 +public class OutputView { + + // 스트라이크, 볼 개수에 따라 결과 문자열을 출력 + public void printResult(Result result) { + if (result.isNothing()) { + System.out.println("낫싱"); + return; + } + + String message = buildMessage(result); + System.out.println(message); + } + + // 스트라이크, 볼 개수에 맞는 메시지 생성 + private String buildMessage(Result result) { + StringBuilder builder = new StringBuilder(); + + int strikes = result.strikes(); + if (strikes > 0) { + builder.append(strikes) + .append("스트라이크 "); + } + + int balls = result.balls(); + if (balls > 0) { + builder.append(balls) + .append("볼"); + } + + return builder.toString().trim(); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/domain/JudgeTest.java b/src/test/java/baseball/domain/JudgeTest.java new file mode 100644 index 00000000..b2968f74 --- /dev/null +++ b/src/test/java/baseball/domain/JudgeTest.java @@ -0,0 +1,60 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class JudgeTest { + + @DisplayName("숫자와 자리가 모두 같으면 스트라이크로 판정") + @Test + void judge_all_strikes() { + ComputerNumbers answer = new ComputerNumbers(new int[]{1, 2, 3}); + Guess guess = Guess.from("123"); + + Result result = new Judge().judge(answer, guess); + + assertThat(result.strikes()).isEqualTo(3); + assertThat(result.balls()).isZero(); + assertThat(result.isThreeStrikes()).isTrue(); + } + + @DisplayName("자리는 다르지만 같은 숫자면 볼로 판정") + @Test + void judge_only_balls() { + ComputerNumbers answer = new ComputerNumbers(new int[]{1, 2, 3}); + Guess guess = Guess.from("312"); + + Result result = new Judge().judge(answer, guess); + + assertThat(result.strikes()).isZero(); + assertThat(result.balls()).isEqualTo(3); + assertThat(result.isNothing()).isFalse(); + } + + @DisplayName("스트라이크와 볼이 함께 존재할 수 있다") + @Test + void judge_strikes_and_balls() { + ComputerNumbers answer = new ComputerNumbers(new int[]{1, 2, 3}); + Guess guess = Guess.from("132"); + + Result result = new Judge().judge(answer, guess); + + assertThat(result.strikes()).isEqualTo(1); + assertThat(result.balls()).isEqualTo(2); + } + + @DisplayName("겹치는 숫자가 하나도 없으면 낫싱") + @Test + void judge_nothing() { + ComputerNumbers answer = new ComputerNumbers(new int[]{1, 2, 3}); + Guess guess = Guess.from("456"); + + Result result = new Judge().judge(answer, guess); + + assertThat(result.strikes()).isZero(); + assertThat(result.balls()).isZero(); + assertThat(result.isNothing()).isTrue(); + } +} From c279bc722181ec4c2d8c7a590c0ed6d053163a68 Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Fri, 30 Jan 2026 18:44:01 +0900 Subject: [PATCH 5/6] feat(game): add game end/restart flow print message on 3 strikes and prompt user to restart(1) or terminate(2) Refs: README feature #4 --- .../baseball/BaseballGameApplication.java | 1 - .../baseball/controller/GameController.java | 47 ++++++++++++++++--- src/main/java/baseball/domain/GuessError.java | 3 +- src/main/java/baseball/view/InputView.java | 5 ++ src/main/java/baseball/view/OutputView.java | 20 ++++++++ 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/main/java/baseball/BaseballGameApplication.java b/src/main/java/baseball/BaseballGameApplication.java index 6af0e8ac..39b78e48 100644 --- a/src/main/java/baseball/BaseballGameApplication.java +++ b/src/main/java/baseball/BaseballGameApplication.java @@ -15,6 +15,5 @@ public static void main(String[] args) { // 숫자 야구 게임 실행 gameController.run(); - } } \ No newline at end of file diff --git a/src/main/java/baseball/controller/GameController.java b/src/main/java/baseball/controller/GameController.java index d4da5c4c..67400406 100644 --- a/src/main/java/baseball/controller/GameController.java +++ b/src/main/java/baseball/controller/GameController.java @@ -6,6 +6,8 @@ import java.io.IOException; +import static baseball.domain.GuessError.ONE_OR_TWO; + public class GameController { private final NumbersGenerator generator; private final InputView inputView; @@ -19,19 +21,52 @@ public GameController(NumbersGenerator generator, InputView inputView, OutputVie this.judge = judge; // 스트라이크, 볼 판단 } + // 게임 실행 public void run() { + while (true) { + // 한 판 게임 시작 + playOneGame(); + + // 1이면 재시작, 2이면 종료 + boolean restart = askToRestart(); + if (restart) continue; + return; + } + } + + // 한 판 게임 실행(컴퓨터 숫자 생성 → 입력/판정 반복 → 3스트라이크 시 종료) + private void playOneGame() { // 컴퓨터가 생성한 숫자 - ComputerNumbers computerNumbers = generator.generate(); - System.out.println(computerNumbers); + ComputerNumbers answer = generator.generate(); + // 올바른 사용자 input이 들어오면 스트라이크/볼 판정 후 3스트라이크가 되면 게임 종료 while (true) { - // 유저가 입력한 숫자 Guess guess = readGuessUntilValid(); - System.out.println(guess); - Result result = judge.judge(computerNumbers, guess); + Result result = judge.judge(answer, guess); outputView.printResult(result); - if (result.isThreeStrikes()) return; // 3스트라이크면 종료 + if (result.isThreeStrikes()) { + outputView.printWinMessage(); + return; + } + } + } + + // 재시작 여부를 묻고 1이면 true, 2면 false를 반환 (유효한 입력이 들어올 때까지 재시도) + private boolean askToRestart() { + while (true) { + try { + outputView.printRestartMessage(); + String input = inputView.readRestart(); + if ("1".equals(input)) return true; + if ("2".equals(input)) { + outputView.printGameTerminateMessage(); + return false; + } + outputView.printError(ONE_OR_TWO.message()); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/baseball/domain/GuessError.java b/src/main/java/baseball/domain/GuessError.java index 4090d7a8..ed8e2c6d 100644 --- a/src/main/java/baseball/domain/GuessError.java +++ b/src/main/java/baseball/domain/GuessError.java @@ -5,7 +5,8 @@ public enum GuessError { NULL_INPUT("[ERROR] 입력값이 null입니다."), INVALID_LENGTH("[ERROR] 3자리 숫자를 입력해야 합니다."), NON_DIGIT("[ERROR] 숫자만 입력할 수 있습니다."), - OUT_OF_RANGE("[ERROR] 1~9 범위의 숫자만 입력할 수 있습니다."); + OUT_OF_RANGE("[ERROR] 1~9 범위의 숫자만 입력할 수 있습니다."), + ONE_OR_TWO("[ERROR] 1 또는 2를 입력해야 합니다."); private final String message; diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java index dc818f60..063f58c2 100644 --- a/src/main/java/baseball/view/InputView.java +++ b/src/main/java/baseball/view/InputView.java @@ -13,4 +13,9 @@ public String readGuess() throws IOException { System.out.print("숫자를 입력해주세요 : "); return br.readLine(); } + + // 재시작 여부 입력 수집 (1 또는 2) + public String readRestart() throws IOException { + return br.readLine(); + } } diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java index 9fec3830..d815d3be 100644 --- a/src/main/java/baseball/view/OutputView.java +++ b/src/main/java/baseball/view/OutputView.java @@ -34,4 +34,24 @@ private String buildMessage(Result result) { return builder.toString().trim(); } + + // 사용자가 3스트라이크를 맞혔을 때 출력 + public void printWinMessage() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝"); + } + + // 게임 재시작 안내 출력 + public void printRestartMessage() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + } + + // 게임 종료 메시지 출력 + public void printGameTerminateMessage() { + System.out.println("게임을 종료하겠습니다."); + } + + // [ERROR] 메시지 출력용 편의 메서드 + public void printError(String message) { + System.out.println(message); + } } \ No newline at end of file From 32b91a2eb179c511b32a1b6c97f65198cd82b351 Mon Sep 17 00:00:00 2001 From: "ian.lee2" Date: Fri, 30 Jan 2026 18:51:31 +0900 Subject: [PATCH 6/6] refactor(domain): randomize order of generated computer numbers shuffle generated digits to avoid ordered sequence bias Refs: README feature #1 --- .../java/baseball/domain/NumbersGenerator.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/baseball/domain/NumbersGenerator.java b/src/main/java/baseball/domain/NumbersGenerator.java index d8168636..3f860b44 100644 --- a/src/main/java/baseball/domain/NumbersGenerator.java +++ b/src/main/java/baseball/domain/NumbersGenerator.java @@ -1,8 +1,6 @@ package baseball.domain; -import java.util.HashSet; -import java.util.Random; -import java.util.Set; +import java.util.*; /** * 컴퓨터가 사용할 숫자 3개를 생성하는 역할을 담당 @@ -40,14 +38,14 @@ private int randomNumber() { // 생성된 숫자 Set를 배열 형태로 변환 private int[] toArray(Set numbers) { - int[] result = new int[SIZE]; - int index = 0; + // 오름차순으로되는 것을 막기 위해 shuffle로 순서 섞기 로직 추가 + List list = new ArrayList<>(numbers); + Collections.shuffle(list, random); - for (int number : numbers) { - result[index] = number; - index++; + int[] result = new int[SIZE]; + for (int i = 0; i < SIZE; i++) { + result[i] = list.get(i); } - return result; } }