Skip to content
Open

Kai #172

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
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# java-baseball-precourse
# 숫자 야구 게임

## 프로젝트 설명

- 1~9 사이 서로 다른 3자리 숫자를 맞히는 숫자 야구 게임을 구현한다.
- 사용자는 컴퓨터가 만든 3자리 숫자를 맞힐 때까지 입력을 반복한다.
- 입력 결과에 따라 스트라이크/볼/낫싱 힌트를 출력한다.
- 3스트라이크가 되면 게임이 종료되며, 재시작 또는 완전 종료를 선택한다.

## 구현해야 하는 기능

- 컴퓨터의 3자리 숫자 생성 (1~9, 중복 없음)
- 사용자 입력 처리
- 3자리 숫자 입력
- 1~9 범위 확인
- 중복 여부 확인
- 잘못된 입력 시 `[ERROR]` 메시지 출력 후 재입력
- 결과 판정
- 스트라이크: 같은 숫자 & 같은 자리
- 볼: 같은 숫자 & 다른 자리
- 낫싱: 같은 숫자 없음
- 게임 종료 처리
- 3스트라이크 시 종료 메시지 출력
- 재시작(1) / 종료(2) 입력 처리

## 설계

### 패키지 구조 (MVC)

```
baseball/
BaseballGameApplication.java
controller/
BaseballGameController.java
domain/
BaseballNumber.java
BaseballResult.java
view/
InputView.java
OutputView.java
```

### 역할 분리

- **domain**: 숫자 생성/검증, 결과 계산 등 핵심 로직
- **controller**: 게임 진행 흐름 제어
- **view**: 입력 안내/결과 출력

### 핵심 클래스 책임

- `BaseballNumber`: 3자리 숫자 검증 및 생성
- `BaseballResult`: 스트라이크/볼 계산 결과 표현
- `BaseballNumber.random()`: 컴퓨터 숫자 생성
- `BaseballGameController`: 게임 루프, 입력 검증, 재시작 처리
- `InputView` / `OutputView`: 콘솔 입출력 전담

### 구현 순서

1. 전체 뼈대 생성
- 패키지/클래스 생성
- 메서드 시그니처만 두고 내부 로직은 TODO 수준으로 둔다.
2. 도메인 내부 로직 구현
- 숫자 생성/검증, 스트라이크/볼 계산
3. Controller 상세 구현
- 게임 루프, 입력 검증, 재시작 처리
4. 출력 포맷 마무리
- 결과 출력 규칙과 안내 메시지 정리
5. 테스트 코드 작성
- 도메인 로직 단위 테스트(JUnit5 + AssertJ)
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ repositories {
}

dependencies {
// lombok
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'

testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testImplementation 'org.assertj:assertj-core:3.25.3'
testCompileOnly 'org.projectlombok:lombok:1.18.34'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'
}

test {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/baseball/BaseballGameApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package baseball;

import baseball.controller.BaseballGameController;

public class BaseballGameApplication {
public static void main(String[] args) {
BaseballGameController controller = BaseballGameController.create();
controller.play();
}
}
75 changes: 75 additions & 0 deletions src/main/java/baseball/controller/BaseballGameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package baseball.controller;

import baseball.domain.BaseballNumber;
import baseball.domain.BaseballResult;
import baseball.view.InputView;
import baseball.view.OutputView;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class BaseballGameController {
private final InputView inputView;
private final OutputView outputView;

public static BaseballGameController create() {
return new BaseballGameController(new InputView(), new OutputView());
}

public void play() {
while (true) {
BaseballNumber answer = BaseballNumber.random();
playGame(answer);
if (shouldEnd()) {
return;
}
}
}

private void playGame(BaseballNumber answer) {
while (true) {
BaseballNumber guess = readValidGuess();
BaseballResult result = BaseballResult.of(answer, guess);
printResult(result);
if (result.isGameEnd()) {
outputView.printGameEnd();
return;
}
}
}

private BaseballNumber readValidGuess() {
while (true) {
try {
String input = inputView.readGuess();
return BaseballNumber.from(input);
} catch (IllegalArgumentException e) {
outputView.printError(e.getMessage());
}
}
}

private boolean shouldEnd() {
while (true) {
try {
String input = inputView.readRestartCommand();
return parseRestartCommand(input);
} catch (IllegalArgumentException e) {
outputView.printError(e.getMessage());
}
}
}

private void printResult(BaseballResult result) {
outputView.printResult(result);
}

private boolean parseRestartCommand(String input) {
if ("1".equals(input)) {
return false;
}
if ("2".equals(input)) {
return true;
}
throw new IllegalArgumentException("1 또는 2만 입력할 수 있습니다.");
}
}
54 changes: 54 additions & 0 deletions src/main/java/baseball/domain/BaseballNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package baseball.domain;

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

public record BaseballNumber(List<Integer> digits) {
public static BaseballNumber from(String input) {
return new BaseballNumber(validateDigits(input));
}

public static BaseballNumber random() {
Random random = new Random();
List<Integer> digits = new ArrayList<>();
while (digits.size() < 3) {
int digit = random.nextInt(9) + 1;
if (digits.contains(digit)) {
continue;
}
digits.add(digit);
}
return new BaseballNumber(digits);
}

private static List<Integer> validateDigits(String input) {
if (input == null) {
throw new IllegalArgumentException("입력이 비어있습니다.");
}
String value = input.trim();
if (value.length() != 3) {
throw new IllegalArgumentException("입력은 3자리 숫자여야 합니다.");
}
List<Integer> digits = new ArrayList<>();
Set<Integer> unique = new HashSet<>();
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
if (!Character.isDigit(ch)) {
throw new IllegalArgumentException("입력은 숫자만 가능합니다.");
}
int digit = ch - '0';
if (digit == 0) {
throw new IllegalArgumentException("0은 사용할 수 없습니다.");
}
if (!unique.add(digit)) {
throw new IllegalArgumentException("중복된 숫자가 있습니다.");
}
digits.add(digit);
}
return digits;
}

}
27 changes: 27 additions & 0 deletions src/main/java/baseball/domain/BaseballResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package baseball.domain;

public record BaseballResult(int strike, int ball) {
public static BaseballResult of(BaseballNumber computer, BaseballNumber guess) {
int strike = 0;
int ball = 0;
for (int i = 0; i < computer.digits().size(); i++) {
int guessDigit = guess.digits().get(i);
if (computer.digits().get(i).equals(guessDigit)) {
strike++;
continue;
}
if (computer.digits().contains(guessDigit)) {
ball++;
}
}
return new BaseballResult(strike, ball);
}

public boolean isNothing() {
return strike == 0 && ball == 0;
}

public boolean isGameEnd() {
return strike == 3;
}
}
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 final Scanner scanner = new Scanner(System.in);

public String readGuess() {
System.out.print("숫자를 입력해주세요 : ");
return scanner.nextLine().trim();
}

public String readRestartCommand() {
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
return scanner.nextLine().trim();
}
}
49 changes: 49 additions & 0 deletions src/main/java/baseball/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package baseball.view;

import baseball.domain.BaseballResult;

public class OutputView {
public void printResult(BaseballResult result) {
System.out.println(formatResult(result));
}

public void printGameEnd() {
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝");
}

public void printError(String message) {
System.out.println("[ERROR] " + message);
}

private String formatResult(BaseballResult result) {
if (result.isNothing()) {
return "낫싱";
}
StringBuilder builder = new StringBuilder();
appendStrike(builder, result.strike());
appendBall(builder, result.ball());
return builder.toString().trim();
}

private void appendStrike(StringBuilder builder, int strike) {
if (strike == 0) {
return;
}
builder.append(strike).append("스트라이크");
}

private void appendBall(StringBuilder builder, int ball) {
if (ball == 0) {
return;
}
appendSpaceIfNeeded(builder);
builder.append(ball).append("볼");
}

private void appendSpaceIfNeeded(StringBuilder builder) {
if (builder.isEmpty()) {
return;
}
builder.append(' ');
}
}
59 changes: 59 additions & 0 deletions src/test/java/baseball/domain/BaseballNumberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package baseball.domain;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;

class BaseballNumberTest {
@Test
void createFromValidInput() {
BaseballNumber number = BaseballNumber.from("123");
assertThat(number.digits()).containsExactly(1, 2, 3);
}

@Test
void rejectNullInput() {
assertThatThrownBy(() -> BaseballNumber.from(null))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void rejectNonNumericInput() {
assertThatThrownBy(() -> BaseballNumber.from("12a"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void rejectWrongLength() {
assertThatThrownBy(() -> BaseballNumber.from("12"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void rejectZeroDigit() {
assertThatThrownBy(() -> BaseballNumber.from("102"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void rejectDuplicateDigits() {
assertThatThrownBy(() -> BaseballNumber.from("112"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void randomHasThreeUniqueDigits() {
BaseballNumber number = BaseballNumber.random();
List<Integer> digits = number.digits();
assertThat(digits).hasSize(3);
Set<Integer> unique = new HashSet<>(digits);
assertThat(unique).hasSize(3);
for (int digit : digits) {
assertThat(digit).isBetween(1, 9);
}
}
}
Loading