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

## 📑 기능 목록

### 1. 게임 시작(초기화) 시 난수 생성
* 게임 시작 시 1에서 9까지 서로 다른 임의의 수 3개를 선택하여 컴퓨터의 숫자를 생성한다.

### 2. 사용자 입력 및 유효성 검사
* `숫자를 입력해주세요 : ` 문구를 출력하고 사용자로부터 3자리의 숫자를 입력받는다.
* 사용자가 유효하지 않은 값을 입력할 경우 `[ERROR]` 메시지를 출력한다.
* (검증 기준: 숫자가 아닌 값, 3자리가 아닌 경우, 1~9 범위를 벗어난 수)
* 사용자의 경우 3자리 각 숫자가 서로 달라야 한다는 조건은 없다.

### 3. 볼/스트라이크 판정 및 결과 출력
* 컴퓨터의 수와 플레이어의 수를 비교하여 결과를 판정한다.
* 같은 수가 같은 자리에 있으면 `스트라이크`
* 같은 수가 다른 자리에 있으면 `볼`
* 같은 수가 전혀 없으면 `낫싱`
* 판정 결과를 `1스트라이크 1볼`, `낫싱`, `3스트라이크`와 같은 형식으로 출력.

### 4. 게임 종료 및 재시작
* 3스트라이크가 되면 `3개의 숫자를 모두 맞히셨습니다! 게임 끝` 메시지를 출력하며 게임을 종료한다.
* 게임 종료 후 `게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.` 문구를 출력한다.
* 입력값에 따라 게임을 재시작(1)하거나 완전히 종료(2)한다.
19 changes: 19 additions & 0 deletions src/main/java/baseball/BaseballGameApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package baseball;

import baseball.controller.GameController;
import baseball.domain.Judge;
import baseball.domain.NumbersGenerator;
import baseball.view.InputView;
import baseball.view.OutputView;

// 숫자 야구 게임 시작점
public class BaseballGameApplication {

public static void main(String[] args) {
// 게임 실행 흐름을 제어하는 컨트롤러 구성
GameController gameController = new GameController(new NumbersGenerator(), new InputView(), new OutputView(), new Judge());

// 숫자 야구 게임 실행
gameController.run();
}
}
85 changes: 85 additions & 0 deletions src/main/java/baseball/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package baseball.controller;

import baseball.domain.*;
import baseball.view.InputView;
import baseball.view.OutputView;

import java.io.IOException;

import static baseball.domain.GuessError.ONE_OR_TWO;

public class GameController {
private final NumbersGenerator generator;
private final InputView inputView;
private final OutputView outputView;
private final Judge judge;

public GameController(NumbersGenerator generator, InputView inputView, OutputView outputView, Judge judge) {
this.generator = generator; // 컴퓨터 숫자 생성기
this.inputView = inputView; // 사용자 입력 담당 뷰
this.outputView = outputView; // 결과 출력 담당 뷰
this.judge = judge; // 스트라이크, 볼 판단
}

// 게임 실행
public void run() {
while (true) {
// 한 판 게임 시작
playOneGame();

// 1이면 재시작, 2이면 종료
boolean restart = askToRestart();
if (restart) continue;
return;
}
}

// 한 판 게임 실행(컴퓨터 숫자 생성 → 입력/판정 반복 → 3스트라이크 시 종료)
private void playOneGame() {
// 컴퓨터가 생성한 숫자
ComputerNumbers answer = generator.generate();

// 올바른 사용자 input이 들어오면 스트라이크/볼 판정 후 3스트라이크가 되면 게임 종료
while (true) {
Guess guess = readGuessUntilValid();
Result result = judge.judge(answer, guess);
outputView.printResult(result);

if (result.isThreeStrikes()) {
outputView.printWinMessage();
return;
}
}
}

// 재시작 여부를 묻고 1이면 true, 2면 false를 반환 (유효한 입력이 들어올 때까지 재시도)
private boolean askToRestart() {
while (true) {
try {
outputView.printRestartMessage();
String input = inputView.readRestart();
if ("1".equals(input)) return true;
if ("2".equals(input)) {
outputView.printGameTerminateMessage();
return false;
}
outputView.printError(ONE_OR_TWO.message());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

// 올바르게 입력할때 까지 재시도 - Guess는 사용자가 입력할 예측값
private Guess readGuessUntilValid() {
while (true) {
try {
return Guess.from(inputView.readGuess());
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/baseball/domain/ComputerNumbers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package baseball.domain;

// 컴퓨터가 뽑은 임의의 수를 하나의 객체로 관리
public class ComputerNumbers {
private final int[] digits;

public ComputerNumbers(int[] digits) {
this.digits = digits;
}

// 특정 위치의 숫자 조회
public int digitAt(int index) {
return digits[index];
}

// 숫자가 포함되어 있는지 여부 반환
public boolean contains(int value) {
for (int digit : digits) {
if (digit == value) {
return true;
}
}
return false;
}

// 디버깅 용
@Override
public String toString() {
return "컴퓨터가 생성한 숫자: " + digits[0] + digits[1] + digits[2];
}
}
78 changes: 78 additions & 0 deletions src/main/java/baseball/domain/Guess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package baseball.domain;

import java.util.Objects;

// 사용자가 입력한 3자리 숫자를 도메인 객체로 표현
public class Guess {
private static final int SIZE = 3;
private static final int MIN = 1;
private static final int MAX = 9;

private final int[] digits;

// 숫자 3자리를 내부 상태로 보관
private Guess(int[] digits) {
this.digits = digits;
}

// 사용자 입력을 순서대로 검증한 뒤 Guess로 변환
public static Guess from(String input) {
String value = validateNotNullAndTrim(input);
validateLength(value);
validateDigits(value);
int[] digits = toDigits(value);
validateRange(digits);
return new Guess(digits);
}

// 지정한 위치의 숫자 반환
public int digitAt(int index) {
return digits[index];
}

// null 여부 확인 후 앞뒤 공백 제거
private static String validateNotNullAndTrim(String input) {
Objects.requireNonNull(input, GuessError.NULL_INPUT.message());
return input.trim();
}

// 입력 문자열의 길이가 정확히 3인지 검증
private static void validateLength(String value) {
if (value.length() != SIZE) {
throw new IllegalArgumentException(GuessError.INVALID_LENGTH.message());
}
}

// 모든 문자가 숫자인지 검증
private static void validateDigits(String value) {
for (int i = 0; i < SIZE; i++) {
if (!Character.isDigit(value.charAt(i))) {
throw new IllegalArgumentException(GuessError.NON_DIGIT.message());
}
}
}

// 각 자리 숫자가 1~9 범위인지 검증
private static void validateRange(int[] digits) {
for (int digit : digits) {
if (digit < MIN || digit > MAX) {
throw new IllegalArgumentException(GuessError.OUT_OF_RANGE.message());
}
}
}

// 문자열의 각 문자를 정수 배열로 변환
private static int[] toDigits(String value) {
int[] result = new int[SIZE];
for (int i = 0; i < SIZE; i++) {
result[i] = value.charAt(i) - '0';
}
return result;
}

// 디버깅 용
@Override
public String toString() {
return "유저가 입력한 숫자: " + digits[0] + digits[1] + digits[2];
}
}
20 changes: 20 additions & 0 deletions src/main/java/baseball/domain/GuessError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package baseball.domain;

// Guess 생성 과정에서 발생할 수 있는 에러 유형
public enum GuessError {
NULL_INPUT("[ERROR] 입력값이 null입니다."),
INVALID_LENGTH("[ERROR] 3자리 숫자를 입력해야 합니다."),
NON_DIGIT("[ERROR] 숫자만 입력할 수 있습니다."),
OUT_OF_RANGE("[ERROR] 1~9 범위의 숫자만 입력할 수 있습니다."),
ONE_OR_TWO("[ERROR] 1 또는 2를 입력해야 합니다.");

private final String message;

GuessError(String message) {
this.message = message;
}

public String message() {
return message;
}
}
45 changes: 45 additions & 0 deletions src/main/java/baseball/domain/Judge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package baseball.domain;

// 컴퓨터 숫자와 사용자 입력을 비교해 결과를 계산
public class Judge {
private static final int SIZE = 3;

// 컴퓨터 숫자와 사용자 입력을 비교해 스트라이크, 볼 개수 계산
public Result judge(ComputerNumbers answer, Guess guess) {
int strikes = countStrikes(answer, guess);
int balls = countBalls(answer, guess);
return new Result(strikes, balls);
}

// 같은 자리에서 같은 숫자인 경우 스트라이크 개수 계산
private int countStrikes(ComputerNumbers answer, Guess guess) {
int count = 0;

for (int index = 0; index < SIZE; index++) {
if (answer.digitAt(index) == guess.digitAt(index)) {
count++;
}
}

return count;
}

// 자리는 다르지만 포함된 숫자인 경우 볼 개수 계산
private int countBalls(ComputerNumbers answer, Guess guess) {
int count = 0;

for (int index = 0; index < SIZE; index++) {
int value = guess.digitAt(index);

if (answer.digitAt(index) == value) {
continue;
}

if (answer.contains(value)) {
count++;
}
}

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

import java.util.*;

/**
* 컴퓨터가 사용할 숫자 3개를 생성하는 역할을 담당
* 책임:
* - 1~9 범위의 숫자를 사용
* - 서로 다른 숫자 3개를 생성
* - 생성된 결과를 ComputerNumbers 객체로 반환
* 숫자의 유효성(범위, 중복 없음)은 생성 과정에서 보장 (단위 테스트 진행)
*/
public class NumbersGenerator {
// 생성할 숫자의 개수
private static final int SIZE = 3;

// 생성 가능한 숫자의 최댓값 (1 ~ 9)
private static final int MAX = 9;

// 난수 생성을 위한 Random 객체
private final Random random = new Random();

// 1~9 범위의 서로 다른 숫자 3개를 생성하여 ComputerNumbers로 반환
public ComputerNumbers generate() {
Set<Integer> numbers = new HashSet<>();

while (numbers.size() < SIZE) {
numbers.add(randomNumber());
}

return new ComputerNumbers(toArray(numbers));
}

// 1~9 범위의 난수 하나를 생성
private int randomNumber() {
return random.nextInt(MAX) + 1; // 1 ~ 9
}

// 생성된 숫자 Set를 배열 형태로 변환
private int[] toArray(Set<Integer> numbers) {
// 오름차순으로되는 것을 막기 위해 shuffle로 순서 섞기 로직 추가
List<Integer> list = new ArrayList<>(numbers);
Collections.shuffle(list, random);

int[] result = new int[SIZE];
for (int i = 0; i < SIZE; i++) {
result[i] = list.get(i);
}
return result;
}
}
Loading