diff --git a/README.md b/README.md index 8d7e8aee..06ca8b53 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +## ⚾ 숫자 야구 게임 (Number Baseball) +JUnit5와 MVC 패턴을 활용하여 구현한 숫자 야구 게임 프로젝트입니다.
+도메인 로직의 완성도를 높이기 위해 단위 테스트를 포함합니다. + +## 🏗️ 아키텍처 설계 (MVC Pattern) +본 프로젝트는 관심사 분리를 위해 MVC(Model-View-Controller) 패턴을 기반으로 설계되었습니다. + +Model: 게임의 핵심 데이터와 비즈니스 로직을 담당합니다. (컴퓨터 숫자 생성, 스트라이크/볼 판정 등) + +View: 사용자 입력을 받고(System.in), 결과를 출력하는(System.out) UI 영역을 담당합니다. + +Controller: Model과 View 사이에서 흐름을 제어합니다. + +## 📌 주요 기능 목록 +### 1. 게임 시스템 (Model) +상대 플레이어 숫자 생성: 1부터 9까지의 서로 다른 임의의 수 3개를 생성합니다.

+힌트 계산 로직: 사용자가 입력한 숫자와 컴퓨터의 숫자를 비교하여 판정 결과를 도출합니다. +- 스트라이크: 숫자와 위치가 모두 일치하는 경우 +- 볼: 숫자는 일치하지만 위치가 다른 경우 +- 낫싱: 일치하는 숫자가 하나도 없는 경우 +- 게임 상태 관리: 3스트라이크 달성 시 라운드 종료 및 재시작/종료 상태를 관리합니다. + +### 2. 입출력 UI (View) +입력 기능: 숫자 입력 및 게임 재시작 여부(1 또는 2)를 입력받습니다. + +출력 기능: +- 게임 시작 문구 출력 +- 판정 결과(힌트) 출력 +- 게임 종료 및 에러 메시지 출력 + +### 3. 게임 흐름 제어 (Controller) +전반적인 게임 진행 루프를 관리합니다. + +사용자의 입력값에 따라 Model을 업데이트하고 View를 통해 결과를 전달합니다. + +### 4. 예외 처리 +사용자가 잘못된 값을 입력할 경우 [ERROR] 문구를 출력하고 게임을 지속합니다. + +중복된 숫자, 3자리가 아닌 입력, 1~9 범위를 벗어난 값 등 + +## 🧪 테스트 계획 (Unit Test) +JUnit5와 AssertJ를 사용하여 UI 로직을 제외한 도메인(Model) 로직에 대한 단위 테스트를 수행합니다. + +컴퓨터 숫자 생성 테스트: 매번 1~9 사이의, 서로 다른 임의의 숫자가 세개가 생성되는지 검증 + +판정 로직 테스트: 입력값에 따라 정해진 힌트(스트라이크, 볼, 낫싱)가 정확히 반환되는지 검증 + +예외 상황 테스트: 잘못된 입력값 입력 시 의도한 대로 에러 처리가 되는지 검증 + +## 🕹️ 실행 방법 및 예시 +``` +숫자를 입력해주세요 : 123 +1스트라이크 +숫자를 입력해주세요 : 456 +1스트라이크 1볼 +숫자를 입력해주세요 : 425 +3스트라이크 +3개의 숫자를 모두 맞히셨습니다! 게임 종료 +게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요. +``` \ 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..849df127 --- /dev/null +++ b/src/main/java/baseball/Application.java @@ -0,0 +1,10 @@ +package baseball; + +import baseball.controller.BaseballController; + +public class Application { + public static void main(String[] args) { + BaseballController baseballController = new BaseballController(); + baseballController.run(); + } +} \ No newline at end of file diff --git a/src/main/java/baseball/controller/BaseballController.java b/src/main/java/baseball/controller/BaseballController.java new file mode 100644 index 00000000..932a4591 --- /dev/null +++ b/src/main/java/baseball/controller/BaseballController.java @@ -0,0 +1,83 @@ +package baseball.controller; + +import baseball.model.*; +import baseball.view.InputView; +import baseball.view.OutputView; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class BaseballController { + private final InputView inputView; + private final OutputView outputView; + private final NumberGenerator numberGenerator; + + public BaseballController() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.numberGenerator = new NumberGenerator(); + } + + public void run() { + outputView.printStartMessage(); + do { + playGame(); + } while (shouldRestart()); + } + + private void playGame() { + BaseballNumbers computerNumbers = new BaseballNumbers(numberGenerator.createRandomNumbers()); + boolean isGameWon = false; + + while (!isGameWon) { + isGameWon = playTurn(computerNumbers); + } + } + + private boolean playTurn(BaseballNumbers computerNumbers) { + try { + BaseballNumbers playerNumbers = parseInput(inputView.readNumbers()); + GameResult result = computerNumbers.compare(playerNumbers); + + outputView.printResult(result); + return checkWin(result); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return false; + } + } + + private boolean checkWin(GameResult result) { + if (result.getStrikes() == 3) { + outputView.printGameEnd(); + return true; + } + return false; + } + + private BaseballNumbers parseInput(String input) { + if (input.length() != 3 || !input.matches("^[1-9]+$")) { + throw new IllegalArgumentException("1~9 사이의 숫자 3개를 입력해야 합니다."); + } + List numbers = Arrays.stream(input.split("")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + if (numbers.stream().distinct().count() != 3) { + throw new IllegalArgumentException("중복된 숫자는 입력할 수 없습니다."); + } + return new BaseballNumbers(numbers); + } + + private boolean shouldRestart() { + try { + String command = inputView.readRestartCommand(); + if (command.equals("1")) return true; + if (command.equals("2")) return false; + throw new IllegalArgumentException("1 또는 2만 입력 가능합니다."); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return shouldRestart(); // 올바른 입력이 올 때까지 재귀 호출 + } + } +} \ No newline at end of file diff --git a/src/main/java/baseball/model/BaseballNumbers.java b/src/main/java/baseball/model/BaseballNumbers.java new file mode 100644 index 00000000..ffa8ea24 --- /dev/null +++ b/src/main/java/baseball/model/BaseballNumbers.java @@ -0,0 +1,33 @@ +package baseball.model; +import java.util.List; + +public class BaseballNumbers { + private final List numbers; + + public BaseballNumbers(List numbers) { + this.numbers = numbers; + } + + public GameResult compare(BaseballNumbers other) { + int strikes = 0; + int balls = 0; + + for (int i = 0; i < numbers.size(); i++) { + if (isStrike(other, i)) { + strikes++; + continue; + } + if (isBall(other, i)) balls++; + } + + return new GameResult(strikes, balls); + } + + private boolean isStrike(BaseballNumbers other, int index) { + return this.numbers.get(index).equals(other.numbers.get(index)); + } + + private boolean isBall(BaseballNumbers other, int index) { + return other.numbers.contains(this.numbers.get(index)); + } +} \ No newline at end of file diff --git a/src/main/java/baseball/model/GameResult.java b/src/main/java/baseball/model/GameResult.java new file mode 100644 index 00000000..d875fa10 --- /dev/null +++ b/src/main/java/baseball/model/GameResult.java @@ -0,0 +1,19 @@ +package baseball.model; + +public class GameResult { + private final int strikes; + private final int balls; + + public GameResult(int strikes, int balls) { + this.strikes = strikes; + this.balls = balls; + } + + public int getStrikes() { + return strikes; + } + + public int getBalls() { + return balls; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/model/NumberGenerator.java b/src/main/java/baseball/model/NumberGenerator.java new file mode 100644 index 00000000..c9411c8d --- /dev/null +++ b/src/main/java/baseball/model/NumberGenerator.java @@ -0,0 +1,30 @@ +package baseball.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class NumberGenerator { + private static final int TARGET_SIZE = 3; + private final Random random = new Random(); + + public List createRandomNumbers() { + List numbers = new ArrayList<>(); + while (numbers.size() < TARGET_SIZE) { + int randomNumber = generateRandomNumber(); + addIfUnique(numbers, randomNumber); + } + return numbers; + } + + private int generateRandomNumber() { + // nextInt(9)는 0~8을 반환하므로 +1을 해서 1~9로 만듭니다. + return random.nextInt(9) + 1; + } + + private void addIfUnique(List numbers, int number) { + if (!numbers.contains(number)) { + numbers.add(number); + } + } +} \ No newline at end of file diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 00000000..d4697301 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,17 @@ +package baseball.view; + +import java.util.Scanner; + +public class InputView { + private static final Scanner scanner = new Scanner(System.in); + + public String readNumbers() { + System.out.print("숫자를 입력해주세요 : "); + return scanner.nextLine(); + } + + public String readRestartCommand() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + return scanner.nextLine(); + } +} \ 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..bf535ec7 --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,33 @@ +package baseball.view; + +import baseball.model.GameResult; + +public class OutputView { + public void printStartMessage() { + System.out.println("숫자 야구 게임을 시작합니다."); + } + + public void printResult(GameResult result) { + if (result.getStrikes() == 0 && result.getBalls() == 0) { + System.out.println("낫싱"); + return; + } + + StringBuilder sb = new StringBuilder(); + if (result.getBalls() > 0) { + sb.append(result.getBalls()).append("볼 "); + } + if (result.getStrikes() > 0) { + sb.append(result.getStrikes()).append("스트라이크"); + } + System.out.println(sb.toString().trim()); + } + + public void printGameEnd() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료"); + } + + public void printErrorMessage(String message) { + System.out.println("[ERROR] " + message); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/BaseballModelTest.java b/src/test/java/baseball/BaseballModelTest.java new file mode 100644 index 00000000..af141732 --- /dev/null +++ b/src/test/java/baseball/BaseballModelTest.java @@ -0,0 +1,74 @@ +package baseball; + +import static org.assertj.core.api.Assertions.assertThat; + +import baseball.model.BaseballNumbers; +import baseball.model.GameResult; +import baseball.model.NumberGenerator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class BaseballModelTest { + + @Test + @DisplayName("생성된 숫자는 반드시 3자리여야 한다") + void generate_SizeTest() { + NumberGenerator generator = new NumberGenerator(); + List numbers = generator.createRandomNumbers(); + + assertThat(numbers).hasSize(3); + } + + @Test + @DisplayName("생성된 숫자는 모두 1에서 9 사이의 값이어야 한다") + void generate_RangeTest() { + NumberGenerator generator = new NumberGenerator(); + List numbers = generator.createRandomNumbers(); + + // 모든 숫자가 1 이상 9 이하인지 검증 + for (int number : numbers) { + assertThat(number).isBetween(1, 9); + } + } + + @Test + @DisplayName("생성된 3개의 숫자는 서로 중복되지 않아야 한다") + void generate_DuplicateTest() { + NumberGenerator generator = new NumberGenerator(); + List numbers = generator.createRandomNumbers(); + + // 중복을 허용하지 않는 Set에 넣었을 때도 크기가 3이어야 함 + Set uniqueNumbers = new HashSet(numbers); + + assertThat(uniqueNumbers).hasSize(3); + } + + @Test + @DisplayName("숫자와 위치가 같으면 '스트라이크'로 판정된다") + void compare_StrikeTest() { + BaseballNumbers computer = new BaseballNumbers(List.of(1, 2, 3)); + BaseballNumbers player = new BaseballNumbers(List.of(1, 2, 5)); + + // When: 비교 로직을 실행하면 + GameResult result = computer.compare(player); + + // Then: 2스트라이크여야 한다 + assertThat(result.getStrikes()).isEqualTo(2); + } + + @Test + @DisplayName("숫자는 같지만 위치가 다르면 '볼'로 판정된다") + void compare_BallTest() { + BaseballNumbers computer = new BaseballNumbers(List.of(1, 2, 3)); + BaseballNumbers player = new BaseballNumbers(List.of(3, 1, 2)); + + // When: 비교 로직을 실행하면 + GameResult result = computer.compare(player); + + // Then: 3볼 0스트라이크여야 한다 + assertThat(result.getBalls()).isEqualTo(3); + } +}