diff --git a/README.md b/README.md index 8d7e8aee..9b95a6b5 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +구현할 기능 목록 +1. 정답 (서로 다른 숫자 3개로 이루어진 3자리수) 생성 +2. 사용자로부터 3자리수 입력받기 + + 다음과 같은 경우 IllegalArgumentException 발생 -> 애플리케이션 종료 + 1. 정수 외의 입력 + 2. 3자리 수 이외의 입력 + 3. 사용자의 입력에 0이 포함된 경우 + +3. 정답과 사용자입력을 비교하여 결과 계산 (낫싱, 1볼1스트라이크 등) +4. 3의 결과 출력 +5. 정답(3스트라이크)인 경우 게임 종료 +6. 사용자 선택 (1/ 2) 입력 받기 + 1. 1을 입력 받은 경우 다음 게임 시작 (반복) + 2. 2를 입력받은 경우 애플리케이션 종료 + \ No newline at end of file diff --git a/src/main/java/app/Application.java b/src/main/java/app/Application.java new file mode 100644 index 00000000..34e333a7 --- /dev/null +++ b/src/main/java/app/Application.java @@ -0,0 +1,15 @@ +package app; + +import game.Game; + +public class Application { + + public static void main(String[] args) { + Game game = new Game(); + try { + game.run(); + } catch (IllegalArgumentException e) { + System.out.println(e); + } + } +} diff --git a/src/main/java/game/AnswerGenerator.java b/src/main/java/game/AnswerGenerator.java new file mode 100644 index 00000000..bb6f3ce5 --- /dev/null +++ b/src/main/java/game/AnswerGenerator.java @@ -0,0 +1,7 @@ +package game; + +public interface AnswerGenerator { + + String getAnswerAsString(); + +} diff --git a/src/main/java/game/Evaluator.java b/src/main/java/game/Evaluator.java new file mode 100644 index 00000000..e1063bbc --- /dev/null +++ b/src/main/java/game/Evaluator.java @@ -0,0 +1,63 @@ +package game; + +public class Evaluator { + + private String answer; + private int nBall; + private int nStrike; + + public Evaluator() { + nBall = 0; + nStrike = 0; + } + + public String getAnswer() { + return answer; + } + + /** + * 상대방(컴퓨터)가 예상한 숫자(정답)을 설정한다. + */ + public void setAnswer(AnswerGenerator answerGenerator) { + answer = answerGenerator.getAnswerAsString(); + } + + public int getNBall() { + return nBall; + } + + public int getNStrike() { + return nStrike; + } + + /** + * 유저가 정답을 맞혔는지 판단한다 + * + * @return 정답을 맞힌 경우 true, 아니면 false + */ + public boolean isCorrect() { + return nStrike == GameParameters.nDigit; + } + + /** + * 유저의 응답을 정답과 비교하여 볼과 스트라이크의 수를 세고 nBall과 nStrike를 업데이트한다. + * + * @param userGuess 유저의 응답 + */ + public int[] evaluate(String userGuess) { + nBall = 0; + nStrike = 0; + int[] ballAndStrike = new int[2]; + for (int i = 0; i < GameParameters.nDigit; i++) { + int idx = answer.indexOf(userGuess.charAt(i)); + if (idx == i) { + nStrike++; + } else if (idx != -1) { + nBall++; + } + } + ballAndStrike[0] = nBall; + ballAndStrike[1] = nStrike; + return ballAndStrike; + } +} diff --git a/src/main/java/game/Game.java b/src/main/java/game/Game.java new file mode 100644 index 00000000..ef0e7ab2 --- /dev/null +++ b/src/main/java/game/Game.java @@ -0,0 +1,59 @@ +package game; + +import java.util.Objects; +import utils.UserInputChecker; +import view.View; + +public class Game { + + private static UserInputChecker userInputChecker = null; + private Evaluator evaluator; + + public Game() { + userInputChecker = UserInputChecker.getUserInputChecker(); + evaluator = new Evaluator(); + } + + + /** + * 다음 판을 진행할 지, 게임을 종료할 지 결정한다 + * + * @return 다음 판을 진행하는 경우 true, 종료하는 경우 false + */ + private boolean doNextGame() { + View.printCorrectAndGameOver(); + String userChoice = View.getUserChoiceForNextGame(); + userInputChecker.isValidChoice(userChoice); + + return Objects.equals(userChoice, GameParameters.nextGame); + } + + /** + * 한 판의 숫자야구 게임을 실행한다 유저가 정답을 맞출 때까지 응답을 입력받고 결과를 출력한다 + */ + private void runSingleGame() { + String userGuess; + while (true) { + userGuess = View.getUserGuess(); + userInputChecker.isValidGuess(userGuess); + evaluator.evaluate(userGuess); + View.printResult(evaluator.getNBall(), evaluator.getNStrike()); + + if (evaluator.isCorrect()) { + break; + } + } + } + + /** + * 여러 번의 게임을 반복적으로 실행한다 매 게임이 시작될 때마다 정답을 세팅한다. 매 게임이 끝날 때마다 다음 게임을 시작하거나, 완전히 종료한다 + */ + public void run() { + boolean continueGame = true; + while (continueGame) { + evaluator.setAnswer(new RandomAnswerGenerator()); + runSingleGame(); + continueGame = doNextGame(); + } + } +} diff --git a/src/main/java/game/GameParameters.java b/src/main/java/game/GameParameters.java new file mode 100644 index 00000000..f188b9eb --- /dev/null +++ b/src/main/java/game/GameParameters.java @@ -0,0 +1,11 @@ +package game; + +public class GameParameters { + + public static final int nDigit = 3; + public static final String nextGame = "1"; + public static final String quitGame = "2"; + + private GameParameters() { + } +} diff --git a/src/main/java/game/RandomAnswerGenerator.java b/src/main/java/game/RandomAnswerGenerator.java new file mode 100644 index 00000000..540c72b2 --- /dev/null +++ b/src/main/java/game/RandomAnswerGenerator.java @@ -0,0 +1,32 @@ +package game; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RandomAnswerGenerator implements AnswerGenerator { + + private List digits; + + public RandomAnswerGenerator() { + digits = new ArrayList<>(9); + for (int i = 1; i < 10; i++) { + digits.add(i); + } + } + + /** + * 1~9까지 다른 숫자 3개로 이루어진 정답을 생성한다. + * + * @return 랜덤으로 생성된 정답 + */ + public String getAnswerAsString() { + String answer = ""; + for (int i = 0; i < GameParameters.nDigit; i++) { + Collections.shuffle(digits); + int d = digits.remove(digits.size() - 1); + answer += d; + } + return answer; + } +} diff --git a/src/main/java/game/TestAnswerGenerator.java b/src/main/java/game/TestAnswerGenerator.java new file mode 100644 index 00000000..2a8da78c --- /dev/null +++ b/src/main/java/game/TestAnswerGenerator.java @@ -0,0 +1,15 @@ +package game; + +public class TestAnswerGenerator implements AnswerGenerator { + + private final String answer; + + public TestAnswerGenerator(String predefinedAnswer) { + answer = predefinedAnswer; + } + + @Override + public String getAnswerAsString() { + return answer; + } +} diff --git a/src/main/java/utils/UserInputChecker.java b/src/main/java/utils/UserInputChecker.java new file mode 100644 index 00000000..96b83569 --- /dev/null +++ b/src/main/java/utils/UserInputChecker.java @@ -0,0 +1,55 @@ +package utils; + +import game.GameParameters; +import java.util.Objects; + +public class UserInputChecker { + + private static UserInputChecker userInputChecker = null; + + private UserInputChecker() { + + } + + public static UserInputChecker getUserInputChecker() { + if (userInputChecker == null) { + userInputChecker = new UserInputChecker(); + } + return userInputChecker; + } + + /** + * 유저가 제출한 숫자의 유효성을 검증한다. + * + * @param userGuess 유저가 제출한 숫자 + * @throws IllegalArgumentException 유저의 응답이 0이나 숫자가 아닌 문자를 포함하는 경우 또는 자릿수가 틀린 경우 + */ + public void isValidGuess(String userGuess) throws IllegalArgumentException { + if (userGuess.length() != GameParameters.nDigit) { + throw new IllegalArgumentException(); + } + char ch; + for (int i = 0; i < GameParameters.nDigit; i++) { + ch = userGuess.charAt(i); + if (!Character.isDigit(ch) || ch == '0') { + throw new IllegalArgumentException(); + } + } + } + + /** + * 유저가 입력한 선택(새게임 또는 종료)을 검증한다. + * + * @param userChoice 유저가 입력한 선택 + * @throws IllegalArgumentException 유저가 옵션에 없는 선택을 한 경우 + */ + public void isValidChoice(String userChoice) throws IllegalArgumentException { + if (Objects.equals(userChoice, GameParameters.nextGame)) { + return; + } + if (Objects.equals(userChoice, GameParameters.quitGame)) { + return; + } + throw new IllegalArgumentException(); + } +} diff --git a/src/main/java/view/View.java b/src/main/java/view/View.java new file mode 100644 index 00000000..169408f7 --- /dev/null +++ b/src/main/java/view/View.java @@ -0,0 +1,61 @@ +package view; + +import java.util.Scanner; + +public class View { + + private static final Scanner scanner = new Scanner(System.in); + + private View() { + + } + + /** + * 유저에게 응답을 입력받는 프롬프트를 출력한다 + * + * @return 유저가 입력한 응답 + */ + public static String getUserGuess() { + String strUserInput; + System.out.print("숫자를 입력하세요 : "); + strUserInput = scanner.nextLine(); + return strUserInput; + } + + /** + * 볼과 스트라이크의 개수를 출력한다 + * + * @param nBall 볼의 개수 + * @param nStrike 스트라이크의 개수 + */ + public static void printResult(int nBall, int nStrike) { + if (nBall == 0 && nStrike == 0) { + System.out.println("낫싱"); + return; + } + if (nBall > 0) { + System.out.print(nBall + "볼"); + } + if (nStrike > 0) { + System.out.print(nStrike + "스트라이크"); + } + System.out.println(); + } + + /** + * 유저가 정답을 맞혔을 때 출력 + */ + public static void printCorrectAndGameOver() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료"); + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + } + + /** + * 게임 종료 시 유저의 선택을 입력받는다 (새게임 또는 게임종료) + * + * @return 유저의 선택 + */ + public static String getUserChoiceForNextGame() { + return scanner.nextLine(); + } +} diff --git a/src/test/java/game/EvaluatorTest.java b/src/test/java/game/EvaluatorTest.java new file mode 100644 index 00000000..d0775113 --- /dev/null +++ b/src/test/java/game/EvaluatorTest.java @@ -0,0 +1,40 @@ +package game; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class EvaluatorTest { + + static Evaluator evaluator; + + @BeforeAll + static void initAll() { + evaluator = new Evaluator(); + AnswerGenerator answerGenerator = new TestAnswerGenerator("123"); + evaluator.setAnswer(answerGenerator); + } + + @DisplayName("evaluate / isCorrect 테스트") + @Test + public void evaluateTest() { + Assertions.assertArrayEquals(new int[]{0, 0}, evaluator.evaluate("456")); + Assertions.assertArrayEquals(new int[]{0, 1}, evaluator.evaluate("178")); + Assertions.assertArrayEquals(new int[]{0, 2}, evaluator.evaluate("923")); + Assertions.assertArrayEquals(new int[]{0, 3}, evaluator.evaluate("123")); + Assertions.assertArrayEquals(new int[]{1, 0}, evaluator.evaluate("289")); + Assertions.assertArrayEquals(new int[]{1, 1}, evaluator.evaluate("283")); + Assertions.assertArrayEquals(new int[]{2, 1}, evaluator.evaluate("213")); + Assertions.assertArrayEquals(new int[]{3, 0}, evaluator.evaluate("231")); + } + + @DisplayName("isCorrect 테스트") + @Test + public void isCorrectTest() { + evaluator.evaluate("123"); + Assertions.assertEquals(3, evaluator.getNStrike()); + Assertions.assertEquals(0, evaluator.getNBall()); + } +} diff --git a/src/test/java/game/RandomAnswerGeneratorTest.java b/src/test/java/game/RandomAnswerGeneratorTest.java new file mode 100644 index 00000000..244c071e --- /dev/null +++ b/src/test/java/game/RandomAnswerGeneratorTest.java @@ -0,0 +1,23 @@ +package game; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class RandomAnswerGeneratorTest { + + RandomAnswerGenerator gen = new RandomAnswerGenerator(); + + @DisplayName("랜덤 정답 생성 테스트") + @Test + public void testRandomAnswerGenerator() { + String answer = gen.getAnswerAsString(); + assertEquals(GameParameters.nDigit, answer.length(), + "생성된 정답의 길이가 GameParameters.nDigit과 다름"); + assertNotEquals(answer.charAt(0), answer.charAt(1)); + assertNotEquals(answer.charAt(1), answer.charAt(2)); + } + +} diff --git a/src/test/java/utils/UserInputCheckerTest.java b/src/test/java/utils/UserInputCheckerTest.java new file mode 100644 index 00000000..7b3d0e8c --- /dev/null +++ b/src/test/java/utils/UserInputCheckerTest.java @@ -0,0 +1,39 @@ +package utils; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class UserInputCheckerTest { + + UserInputChecker userInputChecker = UserInputChecker.getUserInputChecker(); + + @DisplayName("유저 응답 exception Throw 테스트") + @Test + public void throwExceptionWhenGetWrongInput() { + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidGuess("12?")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidGuess("012")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidGuess("1234")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidGuess("12")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidGuess("")); + } + + @DisplayName("게임종료 후 유저 선택 exception Throw 테스트") + @Test + public void throwExceptionWhenGetWrongChoice() { + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidChoice("0")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidChoice("3")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidChoice("12")); + assertThrows(IllegalArgumentException.class, + () -> userInputChecker.isValidChoice("1-")); + } +}