diff --git a/README.md b/README.md index 8d7e8aee..06d455dc 100644 --- a/README.md +++ b/README.md @@ -1 +1,96 @@ -# java-baseball-precourse \ No newline at end of file +# 숫자 야구 게임 기능 구현 목록 + +## 게임 초기화 (컴퓨터) + +* 1에서 9까지의 서로 다른 임의의 수 3개를 생성한다. + +## 사용자 입력 + +* 플레이어로부터 서로 다른 3자리의 수를 입력받는다. +* 게임 종료 후 재시작(1) 또는 종료(2) 여부를 입력받는다. + +## 입력 유효성 검사 + +* 사용자가 잘못된 값을 입력할 경우 [ERROR]로 시작하는 에러 메시지를 출력한다. +* 에러 메시지 출력 후 게임을 종료하지 않고 다시 입력을 받는다. + +## 게임 결과 판정 (볼/스트라이크 계산) + +* 컴퓨터의 수와 플레이어의 수를 비교하여 결과를 계산한다. +* 같은 수가 같은 자리에 있으면 스트라이크. +* 같은 수가 다른 자리에 있으면 볼. +* 같은 수가 전혀 없으면 낫싱. + +## 결과 출력 + +* 계산된 볼, 스트라이크 개수를 출력한다 (예: 1볼 1스트라이크, 낫싱). +* 3개의 숫자를 모두 맞히면(3스트라이크) 게임 종료 문구를 출력한다. + +```mermaid +classDiagram + class Application { + <> + +main(args: String[]): void + } + + class GameFlowController { + <> + -secretModel: SecretNumberModel + -playerInputValidator: PlayerInputValidator + -hintModel: HintCalculatorModel + -gameOutput: GameOutput + -gameInput: GameInput + +start(): void + -askRestart(): boolean + } + + class GameInput { + <> + -scanner: java.util.Scanner + +readLine(): String + } + + class PlayerInputValidator { + <> + +isValidInput(input: String): boolean + +toDigits(input: String): java.util.List + } + + class SecretNumberModel { + <> + -secret: java.util.List + +generateSecret(): void + +getSecret(): java.util.List + } + + class HintCalculatorModel { + <> + +calculate(secret: java.util.List, guess: java.util.List): HintResultModel + } + + class HintResultModel { + <> + -strikes: int + -balls: int + +getStrikes(): int + +toString(): String + } + + class GameOutput { + <> + +showPromptForGuess(): void + +showHint(result: HintResultModel): void + +showWinAndPromptRestart(): void + +showError(message: String): void + +showExitMessage(): void + } + + Application --> GameFlowController: starts + GameFlowController --> SecretNumberModel: "정답 생성 요청" + GameFlowController --> PlayerInputValidator: "입력 검증/숫자 변환 요청" + GameFlowController --> HintCalculatorModel: "판정 로직 실행" + GameFlowController --> GameOutput: "결과/메시지 출력 요청" + GameFlowController --> GameInput: "입력(readLine) 요청" + HintCalculatorModel ..> HintResultModel: returns + GameOutput ..> HintResultModel: displays +``` \ No newline at end of file diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..874a96b1 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,7 @@ +import baseball.GameFlowController; + +public class Application { + public static void main(String[] args) { + new GameFlowController().start(); + } +} diff --git a/src/main/java/baseball/GameFlowController.java b/src/main/java/baseball/GameFlowController.java new file mode 100644 index 00000000..93e362dc --- /dev/null +++ b/src/main/java/baseball/GameFlowController.java @@ -0,0 +1,54 @@ +package baseball; + +import java.util.List; +import java.util.Scanner; + +public class GameFlowController { + private final SecretNumberModel secretModel = new SecretNumberModel(); + private final PlayerInputValidator playerInputValidator = new PlayerInputValidator(); + private final HintCalculatorModel hintModel = new HintCalculatorModel(); + private final GameOutput gameOutput = new GameOutput(); + private final GameInput gameInput = new GameInput(new Scanner(System.in)); + + private boolean askRestart() { + while (true) { + String choice = gameInput.readLine(); + if ("1".equals(choice)) + return true; + if ("2".equals(choice)) + return false; + gameOutput.showError("1 또는 2만 입력 가능합니다."); + } + } + + public void start() { + boolean running = true; + while (running) { + secretModel.generateSecret(); + List secret = secretModel.getSecret(); + + while (true) { + gameOutput.showPromptForGuess(); + String line = gameInput.readLine(); + if (!playerInputValidator.isValidInput(line)) { + gameOutput.showError("입력은 서로 다른 3개의 숫자(1-9)여야 합니다."); + continue; + } + List guess = playerInputValidator.toDigits(line); + HintResultModel hint = hintModel.calculate(secret, guess); + gameOutput.showHint(hint); + if (hint.getStrikes() == 3) { + gameOutput.showWinAndPromptRestart(); + boolean restart = askRestart(); + if (restart) { + break; + } else { + running = false; + break; + } + } + } + } + gameOutput.showExitMessage(); + } +} diff --git a/src/main/java/baseball/GameInput.java b/src/main/java/baseball/GameInput.java new file mode 100644 index 00000000..1d464ba6 --- /dev/null +++ b/src/main/java/baseball/GameInput.java @@ -0,0 +1,16 @@ +package baseball; + +import java.util.Scanner; + +public class GameInput { + + private final Scanner scanner; + + public GameInput(Scanner scanner) { + this.scanner = scanner; + } + + public String readLine() { + return scanner.nextLine().strip(); + } +} diff --git a/src/main/java/baseball/GameOutput.java b/src/main/java/baseball/GameOutput.java new file mode 100644 index 00000000..0ae97bc8 --- /dev/null +++ b/src/main/java/baseball/GameOutput.java @@ -0,0 +1,25 @@ +package baseball; + +public class GameOutput { + + public void showHint(HintResultModel result) { + System.out.println(result.toString()); + } + + public void showError(String message) { + System.out.println("[ERROR] " + message); + } + + public void showWinAndPromptRestart() { + System.out.println("3스트라이크! 승리했습니다."); + System.out.println("게임을 재시작하려면 1, 종료하려면 2를 입력하세요."); + } + + public void showPromptForGuess() { + System.out.println("서로 다른 3자리 숫자를 입력하세요 (각 자리 1-9):"); + } + + public void showExitMessage() { + System.out.println("게임을 종료합니다."); + } +} diff --git a/src/main/java/baseball/HintCalculatorModel.java b/src/main/java/baseball/HintCalculatorModel.java new file mode 100644 index 00000000..9f695ea7 --- /dev/null +++ b/src/main/java/baseball/HintCalculatorModel.java @@ -0,0 +1,18 @@ +package baseball; + +import java.util.List; + +public class HintCalculatorModel { + public HintResultModel calculate(List secret, List guess) { + int strikes = 0; + int balls = 0; + for (int i = 0; i < 3; i++) { + if (guess.get(i).equals(secret.get(i))) { + strikes++; + } else if (secret.contains(guess.get(i))) { + balls++; + } + } + return new HintResultModel(strikes, balls); + } +} diff --git a/src/main/java/baseball/HintResultModel.java b/src/main/java/baseball/HintResultModel.java new file mode 100644 index 00000000..698b3889 --- /dev/null +++ b/src/main/java/baseball/HintResultModel.java @@ -0,0 +1,32 @@ +package baseball; + +public class HintResultModel { + private final int strikes; + private final int balls; + + public HintResultModel(int strikes, int balls) { + this.strikes = strikes; + this.balls = balls; + } + + public int getStrikes() { + return strikes; + } + + @Override + public String toString() { + if (strikes == 0 && balls == 0) + return "낫싱"; + StringBuilder sb = new StringBuilder(); + if (strikes > 0) { + sb.append(strikes).append("스트라이크"); + } + if (balls > 0) { + if (strikes > 0) { + sb.append(" "); + } + sb.append(balls).append("볼"); + } + return sb.toString(); + } +} diff --git a/src/main/java/baseball/PlayerInputValidator.java b/src/main/java/baseball/PlayerInputValidator.java new file mode 100644 index 00000000..584627f3 --- /dev/null +++ b/src/main/java/baseball/PlayerInputValidator.java @@ -0,0 +1,34 @@ +package baseball; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class PlayerInputValidator { + public boolean isValidInput(String s) { + if (s == null) + return false; + if (s.length() != 3) + return false; + HashSet set = new HashSet<>(); + for (char c : s.toCharArray()) { + if (!Character.isDigit(c)) + return false; + int d = c - '0'; + if (d < 1 || d > 9) + return false; + set.add(c); + } + return set.size() == 3; + } + + /* + * 올바른 입력이 주어졌을 때만 호출되어야 합니다. + * */ + public List toDigits(String s) { + List list = new ArrayList<>(); + for (char c : s.toCharArray()) + list.add(c - '0'); + return list; + } +} diff --git a/src/main/java/baseball/SecretNumberModel.java b/src/main/java/baseball/SecretNumberModel.java new file mode 100644 index 00000000..652ea6a3 --- /dev/null +++ b/src/main/java/baseball/SecretNumberModel.java @@ -0,0 +1,21 @@ +package baseball; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SecretNumberModel { + private List secret = new ArrayList<>(); + + public void generateSecret() { + List pool = new ArrayList<>(); + for (int i = 1; i <= 9; i++) + pool.add(i); + Collections.shuffle(pool); + secret = new ArrayList<>(pool.subList(0, 3)); + } + + public List getSecret() { + return new ArrayList<>(secret); + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/baseball/GameInputTest.java b/src/test/java/baseball/GameInputTest.java new file mode 100644 index 00000000..7359c759 --- /dev/null +++ b/src/test/java/baseball/GameInputTest.java @@ -0,0 +1,29 @@ +package baseball; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Scanner; + +import org.junit.jupiter.api.Test; + +class GameInputTest { + + // 트림 기능 테스트 + @Test + void readLine_trims_leading_and_trailing_spaces() { + Scanner scanner = new Scanner(" 123 "); + GameInput input = new GameInput(scanner); + + String result = input.readLine(); + assertEquals("123", result); + } + + // 일반 입력 테스트 + @Test + void readLine_returns_input_as_is() { + Scanner scanner = new Scanner("456"); + GameInput input = new GameInput(scanner); + String result = input.readLine(); + assertEquals("456", result); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/GameOutputTest.java b/src/test/java/baseball/GameOutputTest.java new file mode 100644 index 00000000..3298a6b9 --- /dev/null +++ b/src/test/java/baseball/GameOutputTest.java @@ -0,0 +1,55 @@ +package baseball; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameOutputTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private GameOutput view; + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outContent)); + view = new GameOutput(); + outContent.reset(); + } + + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + + @Test + void showHint_prints_nothing_when_no_match() { + view.showHint(new HintResultModel(0, 0)); + assertEquals("낫싱\n", outContent.toString()); + } + + @Test + void showHint_prints_ball_and_strike() { + view.showHint(new HintResultModel(2, 1)); + assertEquals("2스트라이크 1볼\n", outContent.toString()); + } + + @Test + void showError_prefixes_message_with_error_tag() { + view.showError("입력 오류"); + assertEquals("[ERROR] 입력 오류\n", outContent.toString()); + } + + @Test + void showWinAndPromptRestart_prints_two_lines() { + view.showWinAndPromptRestart(); + String output = outContent.toString(); + assertTrue(output.contains("3스트라이크! 승리했습니다.")); + assertTrue(output.contains("게임을 재시작하려면 1, 종료하려면 2를 입력하세요.")); + } +} diff --git a/src/test/java/baseball/HintCalculatorModelTest.java b/src/test/java/baseball/HintCalculatorModelTest.java new file mode 100644 index 00000000..9404db1e --- /dev/null +++ b/src/test/java/baseball/HintCalculatorModelTest.java @@ -0,0 +1,109 @@ +package baseball; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class HintCalculatorModelTest { + /* + * public HintResultModel calculate(List secret, List guess) { + int strikes = 0; + int balls = 0; + for (int i = 0; i < 3; i++) { + if (guess.get(i).equals(secret.get(i))) { + strikes++; + } else if (secret.contains(guess.get(i))) { + balls++; + } + } + return new HintResultModel(strikes, balls); + } + * */ + + private final HintCalculatorModel hintCalculator = new HintCalculatorModel(); + + // 1. 모든 숫자가 일치하는 경우 (3 스트라이크, 0 볼) + @Test + void calculate_all_match() { + HintResultModel result = hintCalculator.calculate( + List.of(1, 2, 3), + List.of(1, 2, 3) + ); + assert result.getStrikes() == 3; + assert result.toString().equals("3스트라이크"); + } + + // 2. 일부 숫자가 위치까지 일치하는 경우 (예: 2 스트라이크, 0 볼) + @Test + void calculate_partial_strike_match() { + HintResultModel result = hintCalculator.calculate( + List.of(1, 2, 3), + List.of(1, 2, 4) + ); + assert result.getStrikes() == 2; + assert result.toString().equals("2스트라이크"); + } + + @Test + void calculate_partial_strike_match_2() { + HintResultModel result = hintCalculator.calculate( + List.of(4, 5, 6), + List.of(4, 7, 6) + ); + assert result.getStrikes() == 2; + assert result.toString().equals("2스트라이크"); + } + + @Test + void calculate_partial_strike_match_3() { + HintResultModel result = hintCalculator.calculate( + List.of(7, 8, 9), + List.of(0, 8, 9) + ); + assert result.getStrikes() == 2; + assert result.toString().equals("2스트라이크"); + } + + // 3. 일부 숫자가 위치는 다르지만 포함되는 경우 (예: 0 스트라이크, 2 볼) + @Test + void calculate_partial_ball_match() { + HintResultModel result = hintCalculator.calculate( + List.of(1, 2, 3), + List.of(3, 1, 4) + ); + assert result.getStrikes() == 0; + assert result.toString().equals("2볼"); + } + + @Test + void calculate_partial_ball_match_2() { + HintResultModel result = hintCalculator.calculate( + List.of(4, 5, 6), + List.of(6, 4, 7) + ); + assert result.getStrikes() == 0; + assert result.toString().equals("2볼"); + } + + // 4. 숫자가 전혀 일치하지 않는 경우 (0 스트라이크, 0 볼) + @Test + void calculate_no_match() { + HintResultModel result = hintCalculator.calculate( + List.of(1, 2, 3), + List.of(4, 5, 6) + ); + assert result.getStrikes() == 0; + assert result.toString().equals("낫싱"); + } + + @Test + void calculate_no_match_2() { + HintResultModel result = hintCalculator.calculate( + List.of(7, 8, 9), + List.of(1, 2, 3) + ); + assert result.getStrikes() == 0; + assert result.toString().equals("낫싱"); + } + +} \ No newline at end of file diff --git a/src/test/java/baseball/HintResultModelTest.java b/src/test/java/baseball/HintResultModelTest.java new file mode 100644 index 00000000..7310ad36 --- /dev/null +++ b/src/test/java/baseball/HintResultModelTest.java @@ -0,0 +1,70 @@ +package baseball; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class HintResultModelTest { + // 0볼 0스트라이크 + @Test + void testNoBallsNoStrikes() { + HintResultModel hint = new HintResultModel(0, 0); + assertEquals("낫싱", hint.toString()); + } + + // 0볼 1스트라이크 + @Test + void testNoBallsOneStrike() { + HintResultModel hint = new HintResultModel(1, 0); + assertEquals("1스트라이크", hint.toString()); + } + + // 0볼 2스트라이크 + @Test + void testNoBallsTwoStrikes() { + HintResultModel hint = new HintResultModel(2, 0); + assertEquals("2스트라이크", hint.toString()); + } + + // 1볼 0스트라이크 + @Test + void testOneBallNoStrikes() { + HintResultModel hint = new HintResultModel(0, 1); + assertEquals("1볼", hint.toString()); + } + + // 1스트라이크 1볼 (스트라이크 먼저) + @Test + void testOneBallOneStrike() { + HintResultModel hint = new HintResultModel(1, 1); + assertEquals("1스트라이크 1볼", hint.toString()); + } + + // 2볼 0스트라이크 + @Test + void testTwoBallsNoStrikes() { + HintResultModel hint = new HintResultModel(0, 2); + assertEquals("2볼", hint.toString()); + } + + // 1스트라이크 2볼 (스트라이크 먼저) + @Test + void testTwoBallsOneStrike() { + HintResultModel hint = new HintResultModel(1, 2); + assertEquals("1스트라이크 2볼", hint.toString()); + } + + // 3볼 0스트라이크 + @Test + void testThreeBallsNoStrikes() { + HintResultModel hint = new HintResultModel(0, 3); + assertEquals("3볼", hint.toString()); + } + + // 0볼 3스트라이크 + @Test + void testNoBallsThreeStrikes() { + HintResultModel hint = new HintResultModel(3, 0); + assertEquals("3스트라이크", hint.toString()); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/PlayerInputValidatorTest.java b/src/test/java/baseball/PlayerInputValidatorTest.java new file mode 100644 index 00000000..4a3cf9ea --- /dev/null +++ b/src/test/java/baseball/PlayerInputValidatorTest.java @@ -0,0 +1,52 @@ +package baseball; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PlayerInputValidatorTest { + private final PlayerInputValidator validator = new PlayerInputValidator(); + + // - null 입력 -> false 반환 + @Test + void testIsValidInput_NullInput() { + assertFalse(validator.isValidInput(null)); + } + + // - 길이 3이 아닌 입력 (예: "12", "12333", "1234") -> false 반환 + @Test + void testIsValidInput_InvalidLength() { + assertFalse(validator.isValidInput("12")); + assertFalse(validator.isValidInput("12333")); + assertFalse(validator.isValidInput("1234")); + } + + // - 숫자가 아닌 문자 포함 (예: "1a3", "12!") + @Test + void testIsValidInput_NonDigitCharacters() { + assertFalse(validator.isValidInput("1a3")); + } + + // - 1-9 범위 밖의 숫자 포함 (예: "023", "1450") + @Test + void testIsValidInput_OutOfRangeNumbers() { + assertFalse(validator.isValidInput("023")); + assertFalse(validator.isValidInput("1450")); + } + + // - 중복된 숫자 포함 (예: "112", "121") + @Test + void testIsValidInput_DuplicateNumbers() { + assertFalse(validator.isValidInput("112")); + assertFalse(validator.isValidInput("121")); + } + + // - 올바른 입력 (예: "123", "987") -> true + @Test + void testIsValidInput_ValidInput() { + assertTrue(validator.isValidInput("123")); + assertTrue(validator.isValidInput("987")); + } + +} \ No newline at end of file diff --git a/src/test/java/baseball/SecretNumberModelTest.java b/src/test/java/baseball/SecretNumberModelTest.java new file mode 100644 index 00000000..f73de1a4 --- /dev/null +++ b/src/test/java/baseball/SecretNumberModelTest.java @@ -0,0 +1,28 @@ +package baseball; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SecretNumberModelTest { + @Test + void testGenerateSecret() { + SecretNumberModel model = new SecretNumberModel(); + model.generateSecret(); + // 테스트 코드는 여기에서 작성합니다. + // 예: 비어 있지 않은지, 3자리 숫자인지, 중복이 없는지 등을 확인할 수 있습니다. + + List test = model.getSecret(); + Assertions.assertEquals(3, test.size(), "시크릿 넘버는 3자리여야 합니다."); + for (Integer integer : test) { + Assertions.assertNotNull(integer, "시크릿 넘버의 각 자리는 null이 될 수 없습니다."); + Assertions.assertTrue(0 < integer && integer < 10, "시크릿 넘버의 각 자리는 한 자리 자연수여야 합니다."); + } + for (int i = 0; i < test.size(); i++) { + for (int j = i + 1; j < test.size(); j++) { + Assertions.assertNotEquals(test.get(i), test.get(j), "시크릿 넘버의 각 자리는 서로 달라야 합니다."); + } + } + } +} \ No newline at end of file