diff --git a/README.md b/README.md index 8d7e8aee..282c4d66 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +## 프로그램 구현 기능 + +### 프로그램 시작 + +- 숫자 야구 게임을 시작한다. +- 게임이 끝난다면 1 또는 2를 입력받아 새로 시작하거나 종료한다. + +### 숫자야구 게임 시작 + +- 정답 숫자를 생성한다. +- 3자리 수 한 개를 입력받는다. +- 입력받은 수가 3 스트라이크 이면, 숫자 야구 게임을 종료한다. + +### 정답 숫자 생성 + +- 1부터 9까지 서로 다른 수로 이루어진 랜덤한 3자리 수를 생성하여 정답으로 한다. + +### 숫자 입력 + +- 3자리 수를 입력받는다. +- 서로 다른 수로 이루어진 3자리 수가 아니라면 [ERROR]처리한다. + +### 숫자 판단 + +- 스트라이크, 볼, 낫싱을 판단한다. +- 3 스트라이크라면 숫자 야구 게임을 종료한다. + +## 구조 + +### model + +Numbers + +- 숫자 야구에 사용되는 3자리 수 model +- 숫자 입력 값 예외 처리 +- 스트라이크, 볼 계산 + +Game + +- 정답 숫자 생성 +- 정답 숫자와 유저 숫자를 비교하여 스트라이크, 볼, 낫싱 판정 +- 게임 재시작 및 종료 값 예외 처리 + +GameResult + +- 숫자 야구 결과를 출력 + - 스트라이크, 볼, 낫싱 +- 게임 종료 조건(3스트라이크) 확인 + +### view + +InputView + +- 유저 숫자 입력받기 +- 게임 재시작 및 종료 입력받기 + +OutputView + +- 숫자 야구 시작 메시지 출력 +- 숫자 야구 결과 출력 +- 게임 재시작 및 종료 메시지 출력 +- 에러 메시지 출력 + +### controller + +Controller + +- 숫자 야구 실행 +- 숫자 판정 +- 결과 제공 +- 게임 재시작 및 종료 판단 \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 00000000..fa9c8d4a --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,8 @@ +import controller.Controller; + +public class Main { + public static void main(String[] args) { + Controller controller = new Controller(); + controller.run(); + } +} diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 00000000..90439579 --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,51 @@ +package controller; + +import model.Game; +import model.GameResult; +import model.Numbers; +import view.InputView; +import view.OutputView; + +import java.util.List; + +public class Controller { + private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + + public void run() { + while (true) { + int result = play(); + if (result == 2) break; + } + } + + private int play() { + Game game = new Game(); + while (true) { + try { + outputView.OutputInputNumber(); + + Numbers userNumbers = new Numbers(inputView.InputUserNumbers()); + + GameResult result = game.getResult(userNumbers); + outputView.OutputResult(result); + + if (result.isFinish()) { + break; + } + } catch (IllegalArgumentException e) { + outputView.OutputError(e.getMessage()); + } + } + outputView.OutputWin(); + + while (true) { + try { + outputView.OutputInputCommand(); + return game.getCommand(inputView.InputCommand()); + } catch (IllegalArgumentException e) { + outputView.OutputError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/model/Game.java b/src/main/java/model/Game.java new file mode 100644 index 00000000..2d908c96 --- /dev/null +++ b/src/main/java/model/Game.java @@ -0,0 +1,65 @@ +package model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Game { + private final Numbers correctNumbers; + + public Game() { + this.correctNumbers = generateCorrectNumbers(); + } + + private Numbers generateCorrectNumbers() { + List tempNumbers = new ArrayList(); + + for (int i = 1; i <= 9; ++i) { + tempNumbers.add(i); + } + Collections.shuffle(tempNumbers); + + return new Numbers(tempNumbers.subList(0, 3)); + } + + public GameResult getResult(Numbers userNumbers) { + int strike = correctNumbers.countStrike(userNumbers); + int ball = correctNumbers.countBall(userNumbers); + + return new GameResult(strike, ball); + } + + public int getCommand(String commandString) { + return validateAndParseInputString(commandString); + } + + private int validateAndParseInputString(String commandString) { + if (!validateInputLength(commandString)) { + throw new IllegalArgumentException("1 또는 2만 입력해야 합니다."); + } + if (!validateInputIsNumber(commandString)) { + throw new IllegalArgumentException("1 또는 2만 입력해야 합니다."); + } + if (!validateInputIsOneOrTwo(commandString)) { + throw new IllegalArgumentException("1 또는 2만 입력해야 합니다."); + } + return convertStringToInteger(commandString); + } + + private boolean validateInputLength(String commandString) { + return commandString.length() == 1; + } + + private boolean validateInputIsNumber(String commandString) { + return commandString.charAt(0) >= '0' && commandString.charAt(0) <= '9'; + } + + private boolean validateInputIsOneOrTwo(String commandString) { + return commandString.charAt(0) == '1' || commandString.charAt(0) == '2'; + } + + private Integer convertStringToInteger(String commandString) { + return commandString.charAt(0) - '0'; + } + +} diff --git a/src/main/java/model/GameResult.java b/src/main/java/model/GameResult.java new file mode 100644 index 00000000..12789d04 --- /dev/null +++ b/src/main/java/model/GameResult.java @@ -0,0 +1,23 @@ +package model; + +public class GameResult { + private final int strike; + private final int ball; + + public GameResult(int strike, int ball) { + this.strike = strike; + this.ball = ball; + } + + public String getResultString() { + String ret = ""; + if (strike > 0) ret += strike + "스트라이크 "; + if (ball > 0) ret += ball + "볼 "; + if (strike == 0 && ball == 0) ret += "낫싱"; + return ret; + } + + public boolean isFinish() { + return strike == 3; + } +} diff --git a/src/main/java/model/Numbers.java b/src/main/java/model/Numbers.java new file mode 100644 index 00000000..f28a7909 --- /dev/null +++ b/src/main/java/model/Numbers.java @@ -0,0 +1,84 @@ +package model; + +import java.util.ArrayList; +import java.util.List; + +public class Numbers { + private final List numbers; + + public Numbers(String inputNumbersString) { + this.numbers = validateAndParseInputString(inputNumbersString); + } + + public Numbers(List numbers) { + this.numbers = numbers; + } + + private List validateAndParseInputString(String inputNumbersString) { + if (!validateInputLength(inputNumbersString)) { + throw new IllegalArgumentException("3자리여야 합니다."); + } + if (!validateInputIsNumbers(inputNumbersString)) { + throw new IllegalArgumentException("숫자만 입력해야 합니다."); + } + if (!validateInputHasDistinctDigits(inputNumbersString)) { + throw new IllegalArgumentException("중복된 숫자를 다른 자릿수에 사용할 수 없습니다."); + } + return convertStringToIntegerList(inputNumbersString); + } + + private boolean validateInputLength(String inputNumbersString) { + return inputNumbersString.length() == 3; + } + + private boolean validateInputIsNumbers(String inputNumbersString) { + for (int i = 0; i < 3; ++i) { + if (inputNumbersString.charAt(i) < '0' || inputNumbersString.charAt(i) > '9') return false; + } + return true; + } + + private boolean validateInputHasDistinctDigits(String inputNumbersString) { + boolean[] digitsArray = new boolean[13]; + for (int i = 0; i < 3; ++i) { + int digit = inputNumbersString.charAt(i) - '0'; + if (digitsArray[digit]) return false; + digitsArray[digit] = true; + } + return true; + } + + private List convertStringToIntegerList(String inputNumbersString) { + List ret = new ArrayList(); + for (int i = 0; i < 3; ++i) { + ret.add(inputNumbersString.charAt(i) - '0'); + } + return ret; + } + + + public int countStrike(Numbers otherNumbers) { + int cnt = 0; + for (int i = 0; i < 3; ++i) { + if (this.numbers.get(i).equals(otherNumbers.numbers.get(i))) cnt += 1; + } + return cnt; + } + + public int countBall(Numbers otherNumbers) { + boolean[] ballCountArray = new boolean[13]; + int[] ballIndexArray = new int[13]; + int cnt = 0; + + for (int i = 0; i < 3; ++i) { + ballCountArray[this.numbers.get(i)] = true; + ballIndexArray[this.numbers.get(i)] = i; + } + for (int i = 0; i < 3; ++i) { + if (ballCountArray[otherNumbers.numbers.get(i)] && ballIndexArray[otherNumbers.numbers.get(i)] != i) { + cnt += 1; + } + } + return cnt; + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..09ae167e --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,17 @@ +package view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class InputView { + private final Scanner scanner = new Scanner(System.in); + + public String InputUserNumbers() { + return scanner.nextLine(); + } + + public String InputCommand() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..b3e062dc --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,28 @@ +package view; + +import model.Game; +import model.GameResult; +import model.Numbers; + +public class OutputView { + public void OutputInputNumber() { + System.out.print("숫자를 입력해주세요: "); + } + + public void OutputResult(GameResult result) { + System.out.println(result.getResultString()); + } + + public void OutputWin() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝"); + } + + public void OutputInputCommand() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + } + + public void OutputError(String errorMessage) { + System.out.println("[ERROR]: " + errorMessage); + } +} + diff --git a/src/test/java/model/GameResultTest.java b/src/test/java/model/GameResultTest.java new file mode 100644 index 00000000..52d3bdec --- /dev/null +++ b/src/test/java/model/GameResultTest.java @@ -0,0 +1,51 @@ +package model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class GameResultTest { + + @Test + @DisplayName("게임 결과로 2스트라이크 출력") + void getResultString_correct_onlyStrike() { + GameResult result = new GameResult(2, 0); + assertEquals("2스트라이크 ", result.getResultString()); + } + + @Test + @DisplayName("게임 결과로 2스트라이크 출력") + void getResultString_correct_onlyBall() { + GameResult result = new GameResult(0, 1); + assertEquals("1볼 ", result.getResultString()); + } + + @Test + @DisplayName("게임 결과로 2스트라이크 출력") + void getResultString_correct_strikeAndBall() { + GameResult result = new GameResult(1, 2); + assertEquals("1스트라이크 2볼 ", result.getResultString()); + } + + @Test + @DisplayName("게임 결과로 2스트라이크 출력") + void getResultString_correct_nothing() { + GameResult result = new GameResult(0, 0); + assertEquals("낫싱", result.getResultString()); + } + + @Test + @DisplayName("3스트라이크 이면 게임 종료") + void isFinish_correct_finish() { + GameResult result = new GameResult(3, 0); + assertTrue(result.isFinish()); + } + + @Test + @DisplayName("3스트라이크가 아니면 게임 진행") + void isFinish_correct_notFinish() { + GameResult result = new GameResult(2, 1); + assertFalse(result.isFinish()); + } +} diff --git a/src/test/java/model/GameTest.java b/src/test/java/model/GameTest.java new file mode 100644 index 00000000..9440e9c3 --- /dev/null +++ b/src/test/java/model/GameTest.java @@ -0,0 +1,51 @@ +package model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GameTest { + + @Test + @DisplayName("게임 재시작 커맨드 '1' 입력시 1 반환") + void getCommand_one_correct() { + Game game = new Game(); + assertEquals(1, game.getCommand("1")); + } + + @Test + @DisplayName("게임 재시작 커맨드 '2' 입력시 2 반환") + void getCommand_two_correct() { + Game game = new Game(); + assertEquals(2, game.getCommand("2")); + } + + @Test + @DisplayName("길이가 1이 아니면 예외") + void getCommand_fail_invalid_length() { + Game game = new Game(); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> game.getCommand("12")); + assertEquals("1 또는 2만 입력해야 합니다.", e.getMessage()); + } + + @Test + @DisplayName("숫자가 아니면 예외") + void create_fail_notNumber() { + Game game = new Game(); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> game.getCommand("a")); + assertEquals("1 또는 2만 입력해야 합니다.", e.getMessage()); + } + + @Test + @DisplayName("숫자가 아니면 예외") + void create_fail_notOneOrTwo() { + Game game = new Game(); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> game.getCommand("3")); + assertEquals("1 또는 2만 입력해야 합니다.", e.getMessage()); + } +} + diff --git a/src/test/java/model/NumbersTest.java b/src/test/java/model/NumbersTest.java new file mode 100644 index 00000000..88a4187c --- /dev/null +++ b/src/test/java/model/NumbersTest.java @@ -0,0 +1,77 @@ +package model; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NumbersTest { + + @Test + @DisplayName("정상 입력이면 Numbers 생성 성공") + void create_success() { + Numbers stringNumbers = new Numbers("123"); + Numbers integerListNumbers = new Numbers(List.of(1, 2, 3)); + assertNotNull(stringNumbers); + assertNotNull(integerListNumbers); + } + + @Test + @DisplayName("길이가 3보다 짧으면 예외") + void create_fail_short_length() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> new Numbers("12")); + assertEquals("3자리여야 합니다.", e.getMessage()); + } + + @Test + @DisplayName("길이가 3보다 길면 예외") + void create_fail_long_length() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> new Numbers("1245")); + assertEquals("3자리여야 합니다.", e.getMessage()); + } + + @Test + @DisplayName("숫자가 아니면 예외") + void create_fail_notNumber() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> new Numbers("1a3")); + assertEquals("숫자만 입력해야 합니다.", e.getMessage()); + } + + @Test + @DisplayName("중복된 자릿수가 있으면 예외") + void create_fail_duplicate() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> new Numbers("112")); + assertEquals("중복된 숫자를 다른 자릿수에 사용할 수 없습니다.", e.getMessage()); + } + + @Test + @DisplayName("countStrike: 같은 위치 같은 숫자 개수") + void countStrike_success() { + Numbers a = new Numbers("123"); + Numbers b = new Numbers("103"); + assertEquals(2, a.countStrike(b)); + } + + @Test + @DisplayName("countBall: 숫자는 같지만 위치가 다른 개수") + void countBall_success() { + Numbers a = new Numbers("123"); + Numbers b = new Numbers("312"); + assertEquals(3, a.countBall(b)); + } + + @Test + @DisplayName("coundStrikeAndBall: 스트라이크와 볼의 개수") + void countStrikeAndBall_success() { + Numbers a = new Numbers("456"); + Numbers b = new Numbers("475"); + assertEquals(1, a.countStrike(b)); + assertEquals(1, a.countBall(b)); + } +} +