Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,103 @@
# java-baseball-precourse
# 숫자 야구 게임

## 📌 프로젝트 소개
1~9 사이의 서로 다른 숫자 3개로 구성된 정답을 컴퓨터가 생성하고, 사용자는 숫자를 입력하여 스트라이크/볼 힌트를 통해 정답을 맞히는 콘솔 게임입니다.
정답(3 스트라이크)을 맞히면 게임이 종료되며, 사용자는 게임을 재시작(1)하거나 완전히 종료(2)할 수 있습니다.

---

## ✅ 기능 목록

### 게임 흐름
- [x] 게임 시작 시 컴퓨터가 1~9 범위의 서로 다른 숫자 3개를 생성한다.
- [x] 사용자에게 숫자 입력을 요청하고 입력을 받는다.
- [x] 입력한 숫자에 대한 스트라이크/볼/낫싱 힌트를 계산하여 출력한다.
- [x] 3스트라이크가 되면 게임 종료 메시지를 출력한다.
- [x] 게임 종료 후 `1(재시작)` / `2(종료)` 입력을 받아 처리한다.
- [x] 재시작 시 정답 숫자를 새로 생성하여 게임을 다시 진행한다.
- [x] 종료를 선택하면 프로그램을 종료한다.

### 입력 검증 / 예외 처리
- [x] 사용자 입력이 잘못된 경우 `[ERROR]`로 시작하는 에러 메시지를 출력하고 게임을 계속 진행한다.
- [x] 숫자 입력 검증
- [x] 3자리인지 검증한다.
- [x] 각 자리가 1~9 범위인지 검증한다. (도메인: `BaseballNumber`)
- [x] 3자리 숫자가 서로 다른 수인지 검증한다. (도메인: `BaseballNumbers`)
- [x] 재시작/종료 입력 검증
- [x] 1 또는 2만 허용한다.

---

## 🧱 도메인 모델

### `BaseballNumber`
- 한 자리 숫자를 의미하는 값 객체(Value Object)
- 유효 범위: 1~9
- 생성 시 검증을 수행하여 항상 유효한 상태만 유지

### `BaseballNumbers` (일급 컬렉션)
- `BaseballNumber` 3개를 보유하는 일급 컬렉션
- 생성 시 다음 규칙을 검증하여 항상 유효한 상태만 유지
- 숫자 개수는 3개
- 서로 다른 숫자(중복 없음)

### `Hint`
- 스트라이크/볼 결과를 표현하는 값 객체

### `BaseballGame`
- 정답 보유 및 입력과의 비교 결과(`Hint`) 생성

---

## 🧩 패키지 구조(예시)

- `game.baseball.domain`
- `BaseballNumber` : 한 자리 숫자(1~9)
- `BaseballNumbers` : 3자리 숫자 일급 컬렉션(개수/중복 검증)
- `Hint` : 스트라이크/볼 결과 값 객체
- `BaseballGame` : 정답 보유 및 힌트 계산

- `game.baseball.application`
- `BaseballGameService` : 게임 시작 및 추측 처리(유즈케이스)

- `game.baseball.application.port`
- `in` : `BaseballGameUseCase`, `command(GuessCommand, RestartCommand)`
- `out` : `GameInputPort`, `GameOutputPort`, `NumberGeneratorPort`

- `game.baseball.adapter`
- `in` : `BaseballGameInputView`, `BaseballGameOutputView`, `BaseballGameController`
- `out` : `RandomNumberGenerator`, `BaseballPrinter`

- `game`
- `Application` : 프로그램 시작점(의존성 조립)
- `GameRunner` : 실행 트리거

> 핵심 로직(도메인/애플리케이션)과 UI(System.in/out)를 분리하여, 도메인 로직을 단위 테스트 대상으로 삼습니다.

---

## 🧪 테스트 전략
- 도메인 로직에 대해 단위 테스트를 작성합니다.
- UI 로직(System.in/out, Scanner 등)은 테스트 범위에서 제외합니다.
- JUnit5 + AssertJ를 사용합니다.

---

## ⚙️ 프로그래밍 요구사항 준수
- indent depth는 2까지만 허용합니다.
- `else`, `switch/case`를 사용하지 않습니다.
- Java Stream API를 사용하지 않습니다. (람다는 가능)
- 메서드는 15라인을 넘지 않도록 분리합니다.

---

## ▶️ 실행 방법(예시)
- IDE에서 `Application.main()` 실행
- 또는 Gradle 기반 프로젝트라면:
- `./gradlew test`
- `./gradlew run`

---

## ✍️ 커밋/진행 방식
- 기능 구현 전 README에 기능 목록을 작성하고, 기능 단위로 커밋합니다.
9 changes: 9 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import game.GameRunner;
import game.baseball.config.BaseballGameConfig;

public class Application {
public static void main(String[] args) {
GameRunner gameRunner = new BaseballGameConfig().gameRunner();
gameRunner.run();
}
}
6 changes: 6 additions & 0 deletions src/main/java/game/GamePrinter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package game;

public interface GamePrinter {
void printStartingMessage();
void printEndingMessage();
}
17 changes: 17 additions & 0 deletions src/main/java/game/GameRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package game;

public class GameRunner {
private final GamingConsole nowRunningGame;
private final GamePrinter nowRunningGamePrinter;

public GameRunner(GamingConsole nowRunningGame, GamePrinter nowRunningGamePrinter) {
this.nowRunningGame = nowRunningGame;
this.nowRunningGamePrinter = nowRunningGamePrinter;
}

public void run() {
nowRunningGamePrinter.printStartingMessage();
nowRunningGame.play();
nowRunningGamePrinter.printEndingMessage();
}
}
5 changes: 5 additions & 0 deletions src/main/java/game/GamingConsole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package game;

public interface GamingConsole {
void play();
}
33 changes: 33 additions & 0 deletions src/main/java/game/baseball/adapter/RandomNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package game.baseball.adapter;

import game.baseball.application.port.out.NumberGeneratorPort;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class RandomNumberGenerator implements NumberGeneratorPort {
private static final int NUMBER_COUNT = 3;
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 9;
private static final int RANDOM_RANGE = MAX_NUMBER - MIN_NUMBER + 1;

private final Random random = new Random();

@Override
public List<Integer> generate() {
List<Integer> numbers = new ArrayList<>();
while (numbers.size() < NUMBER_COUNT) {
int candidate = random.nextInt(RANDOM_RANGE) + MIN_NUMBER;
addIfAbsent(numbers, candidate);
}
return numbers;
}

private void addIfAbsent(List<Integer> numbers, int candidate) {
if (numbers.contains(candidate)) {
return;
}
numbers.add(candidate);
}
}
87 changes: 87 additions & 0 deletions src/main/java/game/baseball/adapter/in/BaseballGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package game.baseball.adapter.in;

import game.GamingConsole;
import game.baseball.application.port.in.BaseballGameUseCase;
import game.baseball.application.port.in.command.GuessCommand;
import game.baseball.application.port.in.command.RestartCommand;
import game.baseball.application.port.out.GameInputPort;
import game.baseball.application.port.out.GameOutputPort;
import game.baseball.domain.Hint;

public class BaseballGameController implements GamingConsole {
private final BaseballGameUseCase useCase;
private final GameInputPort inputPort;
private final GameOutputPort outputPort;

public BaseballGameController(
BaseballGameUseCase useCase,
GameInputPort inputPort,
GameOutputPort outputPort
) {
this.useCase = useCase;
this.inputPort = inputPort;
this.outputPort = outputPort;
}

@Override
public void play() {
while (true) {
useCase.startNewGame();
playUntilSolved();
if (!restartSelected()) {
return;
}
}
}

private void playUntilSolved() {
while (true) {
Hint hint = readValidHint();
outputPort.showResult(hint);
if (hint.isSolved()) {
outputPort.showGameEnd();
return;
}
}
}

private Hint readValidHint() {
while (true) {
Hint hint = tryGuessOnce();
if (hint != null) {
return hint;
}
}
}

private Hint tryGuessOnce() {
try {
outputPort.showGuessPrompt();
GuessCommand command = inputPort.readGuessCommand();
return useCase.guess(command);
} catch (IllegalArgumentException e) {
outputPort.showError(e.getMessage());
return null;
}
}

private boolean restartSelected() {
while (true) {
Boolean restart = tryReadRestartCommand();
if (restart != null) {
return restart;
}
}
}

private Boolean tryReadRestartCommand() {
try {
outputPort.showRestartPrompt();
RestartCommand command = inputPort.readRestartCommand();
return command.restart();
} catch (IllegalArgumentException e) {
outputPort.showError(e.getMessage());
return null;
}
}
}
81 changes: 81 additions & 0 deletions src/main/java/game/baseball/adapter/in/BaseballGameInputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package game.baseball.adapter.in;

import game.baseball.application.port.out.GameInputPort;
import game.baseball.application.port.in.command.GuessCommand;
import game.baseball.application.port.in.command.RestartCommand;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class BaseballGameInputView implements GameInputPort {
private static final int GUESS_LENGTH = 3;
private static final char DIGIT_ZERO = '0';
private static final String RESTART_COMMAND = "1";
private static final String QUIT_COMMAND = "2";

private final Scanner scanner = new Scanner(System.in);

@Override
public GuessCommand readGuessCommand() {
return new GuessCommand(parseGuess(readLine()));
}

@Override
public RestartCommand readRestartCommand() {
return new RestartCommand(parseRestart(readLine()));
}

private String readLine() {
return scanner.nextLine();
}

private List<Integer> parseGuess(String userInput) {
String input = normalize(userInput);
validateLength(input);
return toDigits(input);
}

private String normalize(String userInput) {
if (userInput == null) {
throw new IllegalArgumentException("입력값이 null 입니다.");
}
return userInput.trim();
}

private void validateLength(String input) {
if (input.length() != GUESS_LENGTH) {
throw new IllegalArgumentException(
String.format("%d자리 숫자를 입력해야 합니다.", GUESS_LENGTH)
);
}
}

private List<Integer> toDigits(String input) {
List<Integer> digits = new ArrayList<>();
for (int i = 0; i < input.length(); i++) {
digits.add(parseDigit(input.charAt(i)));
}
return digits;
}

private int parseDigit(char ch) {
if (!Character.isDigit(ch)) {
throw new IllegalArgumentException("숫자만 입력할 수 있습니다.");
}
return ch - DIGIT_ZERO;
}

private boolean parseRestart(String command) {
String normalized = normalize(command);
if (RESTART_COMMAND.equals(normalized)) {
return true;
}
if (QUIT_COMMAND.equals(normalized)) {
return false;
}
throw new IllegalArgumentException(
String.format("%s 또는 %s를 입력해야 합니다.", RESTART_COMMAND, QUIT_COMMAND)
);
}
}
41 changes: 41 additions & 0 deletions src/main/java/game/baseball/adapter/in/BaseballGameOutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package game.baseball.adapter.in;

import game.baseball.application.port.out.GameOutputPort;
import game.baseball.domain.Hint;

public class BaseballGameOutputView implements GameOutputPort {
private static final int ANSWER_LENGTH = 3;
private static final String RESTART_OPTION = "1";
private static final String QUIT_OPTION = "2";

private static final String NUMBER_INPUT_PROMPT = "숫자를 입력해주세요 : ";
private static final String CORRECT_MESSAGE =
String.format("%d개의 숫자를 모두 맞히셨습니다. 게임 종료", ANSWER_LENGTH);
private static final String OPTION_INPUT_PROMPT =
String.format("게임을 새로 시작하려면 %s, 종료하려면 %s를 입력하세요.", RESTART_OPTION, QUIT_OPTION);

@Override
public void showGuessPrompt() {
System.out.print(NUMBER_INPUT_PROMPT);
}

@Override
public void showResult(Hint hint) {
System.out.println(hint.message());
}

@Override
public void showGameEnd() {
System.out.println(CORRECT_MESSAGE);
}

@Override
public void showRestartPrompt() {
System.out.println(OPTION_INPUT_PROMPT);
}

@Override
public void showError(String message) {
System.out.println("[ERROR] " + message);
}
}
Loading