diff --git a/README.md b/README.md index 8d7e8aee..787b5968 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# java-baseball-precourse \ No newline at end of file +## 구현할 기능 목록 + +### 1. 게임 실행 기능 +- [x] 숫자 생성 기능 +- [x] 스트라이크, 볼 판별 로직 +- [x] 스트라이크, 볼 관련 결과값 저장 + +### 2. 입력 및 출력 관련 +- [x] 숫자 입력 및 결과값 출력 +- [x] 숫자 입력 관련 유효성 검증 로직 도입 + +### 3. 단위 테스트 계획 +- [x] `NumberGeneratorTest` + - [x] 3자리의 숫자인지 검증 + - [x] 각 자리수가 중복되지 않는지 검증 + - [x] 각 자리수가 1-9 사이의 숫자인지 검증 +- [x] `BaseballGameTest` (target: "123") + - [x] 3 스트라이크 (e.g., "123") + - [x] 3 볼 (e.g., "312") + - [x] 1 스트라이크 2 볼 (e.g., "132") + - [x] 낫싱 (e.g., "456") +- [x] `ResultTest` + - [x] 3 스트라이크일 때 `isWin()`이 true인지 검증 + - [x] 3 스트라이크가 아닐 때 `isWin()`이 false인지 검증 + - [x] 0 스트라이크, 0 볼일 때 `isNothing()`이 true인지 검증 \ No newline at end of file diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java new file mode 100644 index 00000000..c3f3a97e --- /dev/null +++ b/src/main/java/baseball/Application.java @@ -0,0 +1,135 @@ +package baseball; + +import java.util.Scanner; + +public class Application { + private static final int GAME_SIZE = 3; + private static final String ERROR_PREFIX = "[ERROR] "; + private static final String INPUT_PROMPT = "숫자를 입력해주세요 : "; + private static final String RESTART_PROMPT = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + private static final String GAME_OVER_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료"; + + public static void main(String[] args) { + Application app = new Application(); + app.run(); + } + + public void run() { + try (Scanner scanner = new Scanner(System.in)) { + play(scanner); + } + } + + private void play(Scanner scanner) { + boolean restart = true; + while (restart) { + String targetNumber = new NumberGenerator().generate(); + playOneGame(scanner, targetNumber); + restart = shouldRestart(scanner); + } + } + + private void playOneGame(Scanner scanner, String targetNumber) { + BaseballGame game = new BaseballGame(targetNumber); + boolean isFinished = false; + while (!isFinished) { + String inputNumber = readUserNumber(scanner); + if (!isValidUserNumber(inputNumber)) { + continue; + } + Result result = game.play(inputNumber); + isFinished = processGameResult(result); + } + } + + private boolean processGameResult(Result result) { + System.out.println(formatResult(result)); + if (result.isWin()) { + System.out.println(GAME_OVER_MESSAGE); + return true; + } + return false; + } + + private String readUserNumber(Scanner scanner) { + System.out.print(INPUT_PROMPT); + return scanner.nextLine(); + } + + private boolean isValidUserNumber(String inputNumber) { + if (inputNumber == null) { + System.out.println(ERROR_PREFIX + "입력값이 올바르지 않습니다."); + return false; + } + if (inputNumber.length() != GAME_SIZE) { + System.out.println(ERROR_PREFIX + "3자리 숫자를 입력해야 합니다."); + return false; + } + if (!areDigitsValid(inputNumber)) { + return false; + } + if (hasDuplicateDigits(inputNumber)) { + System.out.println(ERROR_PREFIX + "서로 다른 숫자를 입력해야 합니다."); + return false; + } + return true; + } + + private boolean areDigitsValid(String inputNumber) { + for (int i = 0; i < GAME_SIZE; i++) { + char c = inputNumber.charAt(i); + if (c < '1' || c > '9') { + System.out.println(ERROR_PREFIX + "1부터 9까지 숫자만 입력해야 합니다."); + return false; + } + } + return true; + } + + private boolean hasDuplicateDigits(String inputNumber) { + for (int i = 0; i < GAME_SIZE; i++) { + if (isCharDuplicatedInRestOfString(inputNumber, i)) { + return true; + } + } + return false; + } + + private boolean isCharDuplicatedInRestOfString(String inputNumber, int index) { + char currentChar = inputNumber.charAt(index); + for (int j = index + 1; j < GAME_SIZE; j++) { + if (currentChar == inputNumber.charAt(j)) { + return true; + } + } + return false; + } + + private boolean shouldRestart(Scanner scanner) { + while (true) { + System.out.println(RESTART_PROMPT); + String input = scanner.nextLine(); + if ("1".equals(input)) { + return true; + } + if ("2".equals(input)) { + return false; + } + System.out.println(ERROR_PREFIX + "1 또는 2를 입력해야 합니다."); + } + } + + private String formatResult(Result result) { + if (result.isNothing()) { + return "낫싱"; + } + StringBuilder res = new StringBuilder(); + if (result.getStrikeCount() > 0) { + res.append(result.getStrikeCount()).append(" 스트라이크 "); + } + if (result.getBallCount() > 0) { + res.append(result.getBallCount()).append(" 볼"); + } + return res.toString().trim(); + } +} diff --git a/src/main/java/baseball/BaseballGame.java b/src/main/java/baseball/BaseballGame.java new file mode 100644 index 00000000..22809894 --- /dev/null +++ b/src/main/java/baseball/BaseballGame.java @@ -0,0 +1,55 @@ +package baseball; + +public class BaseballGame { + private static final int GAME_SIZE = 3; + + private final String targetNumber; + + public BaseballGame(String targetNumber) { + this.targetNumber = targetNumber; + } + + public Result play(String inputNumber) { + int strikeCount = countStrikes(inputNumber); + int ballCount = countBalls(inputNumber); + return new Result(strikeCount, ballCount); + } + + private int countStrikes(String inputNumber) { + int strikeCount = 0; + for (int i = 0; i < GAME_SIZE; i++) { + strikeCount += getStrikeIncrement(inputNumber.charAt(i), i); + } + return strikeCount; + } + + private int getStrikeIncrement(char inputChar, int index) { + if (isStrike(inputChar, index)) { + return 1; + } + return 0; + } + + private int countBalls(String inputNumber) { + int ballCount = 0; + for (int i = 0; i < GAME_SIZE; i++) { + ballCount += getBallIncrement(inputNumber.charAt(i), i); + } + return ballCount; + } + + private int getBallIncrement(char inputChar, int index) { + if (isBall(inputChar, index)) { + return 1; + } + return 0; + } + + private boolean isStrike(char inputChar, int index) { + return targetNumber.charAt(index) == inputChar; + } + + private boolean isBall(char inputChar, int index) { + return !isStrike(inputChar, index) && targetNumber.contains(String.valueOf(inputChar)); + } +} diff --git a/src/main/java/baseball/NumberGenerator.java b/src/main/java/baseball/NumberGenerator.java new file mode 100644 index 00000000..3f4d6698 --- /dev/null +++ b/src/main/java/baseball/NumberGenerator.java @@ -0,0 +1,20 @@ +package baseball; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class NumberGenerator { + public String generate() { + List numbers = new ArrayList<>(); + for (int i = 1; i <= 9; i++) { + numbers.add(i); + } + Collections.shuffle(numbers); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < 3; i++) { + result.append(numbers.get(i)); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/baseball/Result.java b/src/main/java/baseball/Result.java new file mode 100644 index 00000000..6938cb11 --- /dev/null +++ b/src/main/java/baseball/Result.java @@ -0,0 +1,28 @@ +package baseball; + +public class Result { + private final int strikeCount; + private final int ballCount; + + public Result(int strikeCount, int ballCount) { + this.strikeCount = strikeCount; + this.ballCount = ballCount; + } + + public int getStrikeCount() { + return strikeCount; + } + + public int getBallCount() { + return ballCount; + } + + public boolean isNothing() { + return strikeCount == 0 && ballCount == 0; + } + + public boolean isWin() { + return strikeCount == 3; + } +} + diff --git a/src/test/java/baseball/BaseballGameTest.java b/src/test/java/baseball/BaseballGameTest.java new file mode 100644 index 00000000..0ffc687e --- /dev/null +++ b/src/test/java/baseball/BaseballGameTest.java @@ -0,0 +1,40 @@ +package baseball; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class BaseballGameTest { + + private final BaseballGame game = new BaseballGame("123"); + + @ParameterizedTest + @MethodSource("gameScenarios") + @DisplayName("스트라이크와 볼 개수를 정확히 계산한다") + void play_calculatesStrikesAndBallsCorrectly(String input, int expectedStrikes, int expectedBalls) { + Result result = game.play(input); + + assertThat(result.getStrikeCount()).isEqualTo(expectedStrikes); + assertThat(result.getBallCount()).isEqualTo(expectedBalls); + } + + static Stream gameScenarios() { + return Stream.of( + // 계획된 테스트 케이스 + Arguments.of("123", 3, 0), // 3 스트라이크 + Arguments.of("312", 0, 3), // 3 볼 + Arguments.of("132", 1, 2), // 1 스트라이크 2 볼 + Arguments.of("456", 0, 0), // 낫싱 + + // 견고성을 위한 추가 케이스 + Arguments.of("124", 2, 0), // 2 스트라이크 + Arguments.of("415", 0, 1), // 1 볼 + Arguments.of("142", 1, 1) // 1 스트라이크 1 볼 + ); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/NumberGeneratorTest.java b/src/test/java/baseball/NumberGeneratorTest.java new file mode 100644 index 00000000..57db9855 --- /dev/null +++ b/src/test/java/baseball/NumberGeneratorTest.java @@ -0,0 +1,41 @@ +package baseball; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class NumberGeneratorTest { + + private final NumberGenerator generator = new NumberGenerator(); + + @RepeatedTest(10) + @DisplayName("생성된 숫자는 3자리이다") + void generates_a_3_digit_number() { + String number = generator.generate(); + assertThat(number).hasSize(3); + } + + @RepeatedTest(10) + @DisplayName("생성된 숫자는 1-9 사이의 값이다") + void generates_digits_between_1_and_9() { + String number = generator.generate(); + for (char c : number.toCharArray()) { + assertThat(c).isGreaterThanOrEqualTo('1').isLessThanOrEqualTo('9'); + } + } + + @RepeatedTest(10) + @DisplayName("생성된 숫자는 서로 다른 수로 이루어져 있다") + void generates_a_number_with_unique_digits() { + String number = generator.generate(); + Set uniqueDigits = new HashSet<>(); + for (char c : number.toCharArray()) { + uniqueDigits.add(c); + } + assertThat(uniqueDigits).hasSize(3); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/ResultTest.java b/src/test/java/baseball/ResultTest.java new file mode 100644 index 00000000..9a8fd5d6 --- /dev/null +++ b/src/test/java/baseball/ResultTest.java @@ -0,0 +1,57 @@ +package baseball; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Result 클래스") +class ResultTest { + + @Nested + @DisplayName("isWin 메소드는") + class IsWinTest { + + @Test + @DisplayName("3 스트라이크일 때 true를 반환한다") + void returns_true_for_3_strikes() { + Result result = new Result(3, 0); + assertThat(result.isWin()).isTrue(); + } + + @Test + @DisplayName("3 스트라이크가 아닐 때 false를 반환한다") + void returns_false_for_less_than_3_strikes() { + Result resultWith2Strikes = new Result(2, 1); + Result resultWith0Strikes = new Result(0, 2); + + assertThat(resultWith2Strikes.isWin()).isFalse(); + assertThat(resultWith0Strikes.isWin()).isFalse(); + } + } + + @Nested + @DisplayName("isNothing 메소드는") + class IsNothingTest { + + @Test + @DisplayName("0 스트라이크, 0 볼일 때 true를 반환한다") + void returns_true_for_0_strikes_and_0_balls() { + Result result = new Result(0, 0); + assertThat(result.isNothing()).isTrue(); + } + + @Test + @DisplayName("스트라이크나 볼이 하나라도 있으면 false를 반환한다") + void returns_false_if_strikes_or_balls_exist() { + Result resultWithStrikes = new Result(1, 0); + Result resultWithBalls = new Result(0, 2); + Result resultWithBoth = new Result(1, 1); + + assertThat(resultWithStrikes.isNothing()).isFalse(); + assertThat(resultWithBalls.isNothing()).isFalse(); + assertThat(resultWithBoth.isNothing()).isFalse(); + } + } +}