Skip to content
Open
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
# java-baseball-precourse
# java-baseball-precourse

## ⚾ 숫자 야구 게임 (Number Baseball)
JUnit5와 MVC 패턴을 활용하여 구현한 숫자 야구 게임 프로젝트입니다.<br/>
도메인 로직의 완성도를 높이기 위해 단위 테스트를 포함합니다.

## 🏗️ 아키텍처 설계 (MVC Pattern)
본 프로젝트는 관심사 분리를 위해 MVC(Model-View-Controller) 패턴을 기반으로 설계되었습니다.

Model: 게임의 핵심 데이터와 비즈니스 로직을 담당합니다. (컴퓨터 숫자 생성, 스트라이크/볼 판정 등)

View: 사용자 입력을 받고(System.in), 결과를 출력하는(System.out) UI 영역을 담당합니다.

Controller: Model과 View 사이에서 흐름을 제어합니다.

## 📌 주요 기능 목록
### 1. 게임 시스템 (Model)
상대 플레이어 숫자 생성: 1부터 9까지의 서로 다른 임의의 수 3개를 생성합니다.<br><br>
힌트 계산 로직: 사용자가 입력한 숫자와 컴퓨터의 숫자를 비교하여 판정 결과를 도출합니다.
- 스트라이크: 숫자와 위치가 모두 일치하는 경우
- 볼: 숫자는 일치하지만 위치가 다른 경우
- 낫싱: 일치하는 숫자가 하나도 없는 경우
- 게임 상태 관리: 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를 입력하세요.
```
10 changes: 10 additions & 0 deletions src/main/java/baseball/Application.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
83 changes: 83 additions & 0 deletions src/main/java/baseball/controller/BaseballController.java
Original file line number Diff line number Diff line change
@@ -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<Integer> 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(); // 올바른 입력이 올 때까지 재귀 호출
}
}
}
33 changes: 33 additions & 0 deletions src/main/java/baseball/model/BaseballNumbers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package baseball.model;
import java.util.List;

public class BaseballNumbers {
private final List<Integer> numbers;

public BaseballNumbers(List<Integer> 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));
}
}
19 changes: 19 additions & 0 deletions src/main/java/baseball/model/GameResult.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
30 changes: 30 additions & 0 deletions src/main/java/baseball/model/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -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<Integer> createRandomNumbers() {
List<Integer> 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<Integer> numbers, int number) {
if (!numbers.contains(number)) {
numbers.add(number);
}
}
}
17 changes: 17 additions & 0 deletions src/main/java/baseball/view/InputView.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
33 changes: 33 additions & 0 deletions src/main/java/baseball/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
74 changes: 74 additions & 0 deletions src/test/java/baseball/BaseballModelTest.java
Original file line number Diff line number Diff line change
@@ -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<Integer> numbers = generator.createRandomNumbers();

assertThat(numbers).hasSize(3);
}

@Test
@DisplayName("생성된 숫자는 모두 1에서 9 사이의 값이어야 한다")
void generate_RangeTest() {
NumberGenerator generator = new NumberGenerator();
List<Integer> 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<Integer> numbers = generator.createRandomNumbers();

// 중복을 허용하지 않는 Set에 넣었을 때도 크기가 3이어야 함
Set<Integer> uniqueNumbers = new HashSet<Integer>(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);
}
}