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

## 프로젝트 소개
컴퓨터가 1에서 9까지의 서로 다른 임의의 수 3개를 선택하면, 사용자가 그 숫자를 맞추는 게임입니다.

입력한 숫자에 대한 결과(볼, 스트라이크, 낫싱)를 힌트로 얻어 컴퓨터의 수를 모두 맞추면 승리합니다.

## 기능 목록

- **랜덤 숫자 생성**: 1에서 9까지 서로 다른 임의의 수 3개를 선택하여 저장한다.


- **재시작/종료 입력**: 게임 종료 후 재시작(1) 또는 종료(2)를 구분하는 숫자를 입력받는다.


- **사용자 숫자 입력**: 사용자가 예측한 숫자 값을 입력


- **입력값 예외 처리**: 사용자가 잘못된 값을 입력했는지 검증한다.
- 숫자가 아닌 값이 포함된 경우
- 3자리가 아닌 경우
- 1~9 사이의 숫자가 아닌 경우 (0 포함 시)
- 중복된 숫자가 있는 경우


- **판정 로직**: 컴퓨터의 수와 사용자의 수를 비교하여 결과를 계산한다.
- 같은 수가 같은 자리에 있으면 **스트라이크**
- 같은 수가 다른 자리에 있으면 **볼**
- 같은 수가 전혀 없으면 **낫싱**


## 단위 테스트

- **랜덤 숫자 생성 테스트**: 3자리의 서로 다른 숫자가 올바르게 생성되어야 함.
- 생성된 숫자가 3자리인지 확인
- 예: 12, 1234
- 3개 숫자 중 동일한 숫자가 존재
- 예: 112, 1233, 333
- 생성된 숫자에 0 존재
- 예: 012, 0123


- **재시작/종료 입력 테스트**: 사용자는 1 또는 2를 입력해야 함.
- 1, 2를 제외한 다른 숫자를 입력
- 예: 0, 12
- 문자 입력
- 예: kakao


- **사용자 숫자 입력 테스트**: 사용자는 서로 다른 3개의 숫자를 입력해야 함.
- 3자리가 아닌(2자리, 4자리 이상 등) 숫자 입력
- 예: 12, 1234
- 입력한 3개 숫자 중 동일한 숫자가 존재
- 예: 112, 1233, 333
- 입력한 숫자에 0 존재
- 예: 012, 0123
- 문자 입력
- 예: kakao


- **판정 로직 테스트**: 사용자의 입력 값과 실제 값을 올바르게 판정해야 함.
- 여러가지 상황에서 스트라이크, 볼, 낫싱 비교 결과가 일치 (댜양한 case를 만들어보아야 함)
- 예: 428, 624, 921 등..
78 changes: 78 additions & 0 deletions src/main/java/baseball/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package baseball;

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

public class Application {
private final Computer computer;
private final InputView inputView;
private final InputValidator validator;
private final Judge judge;

public Application() {
this.computer = new Computer();
this.inputView = new InputView();
this.validator = new InputValidator();
this.judge = new Judge();
}

public static void main(String[] args) {
new Application().run();
}

public void run() {
System.out.println("숫자 야구 게임을 시작합니다.");
do {
newGame();
} while (isRestart());
System.out.println("숫자 야구 게임을 종료합니다.");
}

private boolean isRestart() {
while (true) {
try {
String input = inputView.inputRestartCommand();
validator.validateRestart(input);
return input.equals("1");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}

public void newGame() {
List<Integer> computerNumbers = computer.generateNumbers();
while (true) {
String userInputStr = getValidInput();
List<Integer> userNumbers = convertToList(userInputStr);

String result = judge.compare(computerNumbers, userNumbers);
System.out.println(result);

if (result.equals("3스트라이크")) {
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝");
break;
}
}
}

private List<Integer> convertToList(String input) {
List<Integer> numbers = new ArrayList<>();
for (char c : input.toCharArray()) {
numbers.add(Character.getNumericValue(c));
}
return numbers;
}

private String getValidInput() {
while (true) {
try {
String input = inputView.inputUserNumber();
validator.validate(input);
return input;
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
}
22 changes: 22 additions & 0 deletions src/main/java/baseball/Computer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package baseball;

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

public class Computer {
public List<Integer> generateNumbers() {
List<Integer> computer = new ArrayList<>();
Random random = new Random();

while (computer.size() < 3) {
int randomNumber = random.nextInt(9) + 1;

if (!computer.contains(randomNumber)) {
computer.add(randomNumber);
}
}
return computer;
}

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

import java.util.HashSet;
import java.util.Set;

public class InputValidator {

public void validate(String input) {
validateLength(input);
validateNumeric(input);
validateRange(input);
validateUnique(input);
}

private void validateLength(String input) {
if (input.length() != 3) {
throw new IllegalArgumentException("[ERROR] 3자리의 숫자를 입력해야 합니다.");
}
}

private void validateNumeric(String input) {
if (!input.matches("\\d+")) {
throw new IllegalArgumentException("[ERROR] 숫자만 입력해야 합니다.");
}
}

private void validateRange(String input) {
if (input.contains("0")) {
throw new IllegalArgumentException("[ERROR] 1에서 9 사이의 숫자여야 합니다.");
}
}

private void validateUnique(String input) {
Set<Character> uniqueChars = new HashSet<>();
for (char c : input.toCharArray()) {
uniqueChars.add(c);
}

if (uniqueChars.size() != 3) {
throw new IllegalArgumentException("[ERROR] 서로 다른 숫자를 입력해야 합니다.");
}
}

public void validateRestart(String input) {
if (!input.equals("1") && !input.equals("2")) {
throw new IllegalArgumentException("[ERROR] 1 또는 2를 입력해야 합니다.");
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/baseball/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package baseball;

import java.util.Scanner;

public class InputView {
private final Scanner scanner;

public InputView() {
this.scanner = new Scanner(System.in);
}

public String inputUserNumber() {
System.out.print("숫자를 입력해 주세요 : ");
return scanner.nextLine(); // 사용자가 친 내용을 문자열로 다 가져옴
}

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

import java.util.List;

public class Judge {

public String compare(List<Integer> computer, List<Integer> user) {
int strikes = countStrikes(computer, user);
int balls = countBalls(computer, user);

if (strikes == 0 && balls == 0) {
return "낫싱";
}
return printResult(balls, strikes);
}

private int countStrikes(List<Integer> computer, List<Integer> user) {
int strikes = 0;
for (int i = 0; i < computer.size(); i++) {
if (computer.get(i).equals(user.get(i))) {
strikes++;
}
}
return strikes;
}

private int countBalls(List<Integer> computer, List<Integer> user) {
int balls = 0;
for (int i = 0; i < computer.size(); i++) {
if (!computer.get(i).equals(user.get(i)) && computer.contains(user.get(i))) {
balls++;
}
}
return balls;
}

private String printResult(int balls, int strikes) {
StringBuilder sb = new StringBuilder();
if (balls > 0) {
sb.append(balls).append("볼 ");
}
if (strikes > 0) {
sb.append(strikes).append("스트라이크");
}
return sb.toString().trim();
}
}
29 changes: 29 additions & 0 deletions src/test/java/baseball/ComputerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package baseball;

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

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

class ComputerTest {

@Test
@DisplayName("생성된 숫자는 3자리여야 하며, 1~9 사이의 중복 없는 수여야 한다")
void computerGenerateRandomCase() {
Computer computer = new Computer();

List<Integer> numbers = computer.generateNumbers();

assertThat(numbers).hasSize(3);

for (Integer number : numbers) {
assertThat(number).isBetween(1, 9);
}

Set<Integer> uniqueNumbers = new HashSet<>(numbers);
assertThat(uniqueNumbers).hasSize(3);
}
}
75 changes: 75 additions & 0 deletions src/test/java/baseball/InputValidatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package baseball;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class InputValidatorTest {

private final InputValidator validator = new InputValidator();

@Test
@DisplayName("정상 입력: 서로 다른 3자리 숫자는 통과해야 한다")
void inputNormal() {
assertThatCode(() -> validator.validate("123"))
.doesNotThrowAnyException();
}

@ParameterizedTest
@ValueSource(strings = {"12", "1234"})
@DisplayName("예외: 3자리가 아닌 숫자 입력 시 예외 발생")
void inputLongCase(String input) {
assertThatThrownBy(() -> validator.validate(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("[ERROR]");
}

@ParameterizedTest
@ValueSource(strings = {"a12", "kakao", "1 3"})
@DisplayName("예외: 숫자가 아닌 문자 입력 시 예외 발생")
void inputStringCase(String input) {
assertThatThrownBy(() -> validator.validate(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("[ERROR]");
}

@ParameterizedTest
@ValueSource(strings = {"112", "121", "333"})
@DisplayName("예외: 중복된 숫자가 있으면 예외 발생")
void inputDuplicateNum(String input) {
assertThatThrownBy(() -> validator.validate(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("[ERROR]");
}

@ParameterizedTest
@ValueSource(strings = {"012", "109"})
@DisplayName("예외: 0이 포함된 숫자 입력 시 예외 발생")
void inputIncludeZeroCase(String input) {
assertThatThrownBy(() -> validator.validate(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("[ERROR]");
}

@Test
@DisplayName("재시작 정상: 1 또는 2 입력 시 통과")
void inputRedoCase() {
assertThatCode(() -> validator.validateRestart("1"))
.doesNotThrowAnyException();
assertThatCode(() -> validator.validateRestart("2"))
.doesNotThrowAnyException();
}

@ParameterizedTest
@ValueSource(strings = {"0", "3", "a", "start"})
@DisplayName("재시작 예외: 1, 2가 아닌 값 입력 시 예외 발생")
void inputWrongRedoCase(String input) {
assertThatThrownBy(() -> validator.validateRestart(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("[ERROR]");
}
}
Loading