diff --git a/README.md b/README.md index 8d7e8aee..e97f8cc8 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +## 구현할 기능 목록 + +- [x] 임의의 난수 3자리 생성하기 +- [x] 사용자 응답 처리 +- [x] 같은 수 & 같은 자리 (스트라이크) 분기 처리 +- [x] 같은 수 & 다른 자리 (볼) 분기 처리 +- [x] 같은 수 없는 경우 (포볼, 낫싱) 분기 처리 +- [x] depth 최대 2가 되도록 리팩토링 +- [x] 단위 테스트 코드 추가하기 diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000..61afb1a4 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,84 @@ +package controller; + +import model.BaseballGameNumber; +import model.GameAnswerType; +import util.IntegerParser; +import util.RandomNumberGenerator; +import view.ErrorMessage; +import view.GameView; +import view.InputView; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; + +public class GameController { + private static final String RESTART_COMMAND = "1"; + private static final String EXIT_COMMAND = "2"; + + private RandomNumberGenerator randomNumberGenerator; + private final GameView gameView; + private final InputView inputView; + private final BufferedReader br; + + public GameController() { + this.randomNumberGenerator = new RandomNumberGenerator(); + this.gameView = new GameView(); + this.inputView = new InputView(gameView); + br = new BufferedReader(new InputStreamReader(System.in)); + } + + public static void main(String[] args) { + try { + new GameController().run(); + } + catch (IOException e) { + System.out.println(ErrorMessage.SYSTEM_IO_ERROR.getMsg()); + } + } + + public void run() throws IOException { + String gameEndUserResponse; + + do { + gameEndUserResponse = playSingleGame(); + } while(Objects.equals(gameEndUserResponse, RESTART_COMMAND)); + + if (!Objects.equals(gameEndUserResponse, EXIT_COMMAND)) { + gameView.printErrorMsg(ErrorMessage.ABNORMAL_EXIT.getMsg()); + return; + } + + gameView.printGameEndMsg(); + + br.close(); + gameView.releaseResource(); + } + + public String playSingleGame() throws IOException { + int answer = randomNumberGenerator.makeRand3digit(); + BaseballGameNumber answerNumber = new BaseballGameNumber(IntegerParser.toIntArray(answer)); + + int userInput = -1; + while (answer != userInput) { + userInput = playTurn(answerNumber); + } + + gameView.printSingleGameEndMsg(); + return br.readLine(); + } + + private int playTurn(BaseballGameNumber answerNumber) throws IOException { + int userInput; + gameView.printTurnMsg(); + userInput = inputView.getUserInput(); + + BaseballGameNumber userNumber = new BaseballGameNumber(IntegerParser.toIntArray(userInput)); + + gameView.printResult(userNumber.compare(answerNumber)); + return userInput; + } +} diff --git a/src/main/java/model/BaseballGameNumber.java b/src/main/java/model/BaseballGameNumber.java new file mode 100644 index 00000000..8510e18a --- /dev/null +++ b/src/main/java/model/BaseballGameNumber.java @@ -0,0 +1,69 @@ +package model; + +import java.util.ArrayList; +import java.util.Objects; + +public class BaseballGameNumber { + private final ArrayList number; + + public BaseballGameNumber(ArrayList arr) { + this.number = arr; + } + + public GameAnswerType compare(BaseballGameNumber other) { + int strikes = calStrikes(other); + int balls = calBalls(strikes, other); + + return new GameAnswerType(strikes, balls); + } + + private int calStrikes(BaseballGameNumber other) { + int strike = 0; + for (int i = 0; i < this.number.size(); i++) { + if(isStrike(i, other)) { + strike += 1; + } + } + + return strike; + } + + private int calBalls(int strikes, BaseballGameNumber other) { + return this.calMatchCount(other) - strikes; + } + + private boolean isStrike(int index, BaseballGameNumber other) { + return Objects.equals(this.number.get(index), other.number.get(index)); + } + + private int calMatchCount(BaseballGameNumber other) { + boolean[] originFlags = this.getNumberFlags(); + boolean[] otherFlags = other.getNumberFlags(); + + int matchCount = 0; + + for (int i = 0; i < originFlags.length; i++) { + matchCount = getMatchCount(originFlags, i, otherFlags, matchCount); + } + + return matchCount; + } + + private static int getMatchCount(boolean[] originFlags, int i, boolean[] otherFlags, int matchCount) { + if(originFlags[i] && otherFlags[i]) { + matchCount += 1; + } + return matchCount; + } + + private boolean[] getNumberFlags() { + boolean[] flags = new boolean[10]; + + for (int num : this.number) { + flags[num] = true; + } + + return flags; + } + +} diff --git a/src/main/java/model/GameAnswerType.java b/src/main/java/model/GameAnswerType.java new file mode 100644 index 00000000..dee87655 --- /dev/null +++ b/src/main/java/model/GameAnswerType.java @@ -0,0 +1,43 @@ +package model; + +public class GameAnswerType { + private final int strikeCount; + private final int ballCount; + + public GameAnswerType() { + this.strikeCount = 0; + this.ballCount = 0; + } + + public GameAnswerType(int strikeCount, int ballCount){ + this.strikeCount = strikeCount; + this.ballCount = ballCount; + } + + public int getStrikeCount() { + return strikeCount; + } + + public int getBallCount() { + return ballCount; + } + + public String getAnswer() { + if (strikeCount + ballCount == 0) { + return "낫싱"; + } + StringBuilder sb = new StringBuilder(); + + if (strikeCount > 0) { + sb.append(strikeCount); + sb.append("스트라이크 "); + } + + if (ballCount > 0) { + sb.append(ballCount); + sb.append("볼 "); + } + + return sb.toString().trim(); + } +} diff --git a/src/main/java/util/IntegerParser.java b/src/main/java/util/IntegerParser.java new file mode 100644 index 00000000..18bef16d --- /dev/null +++ b/src/main/java/util/IntegerParser.java @@ -0,0 +1,18 @@ +package util; + +import java.util.ArrayList; +import java.util.Collections; + +public class IntegerParser { + public static ArrayList toIntArray(int number) { + ArrayList arr = new ArrayList<>(); + + while (number > 0) { + arr.add(number % 10); + number /= 10; + } + Collections.reverse(arr); + + return arr; + } +} diff --git a/src/main/java/util/RandomNumberGenerator.java b/src/main/java/util/RandomNumberGenerator.java new file mode 100644 index 00000000..3d68b1fa --- /dev/null +++ b/src/main/java/util/RandomNumberGenerator.java @@ -0,0 +1,34 @@ +package util; + +import java.util.Random; + +public class RandomNumberGenerator { + private final Random random; + + public RandomNumberGenerator() { + this.random = new Random(); + } + + public int makeRand3digit() { + boolean[] isDuplicated = new boolean[10]; + + int answer = 0; + + for (int i = 0; i < 3; i++) { + answer *= 10; + answer += getNonDuplicatedNumber(isDuplicated); + } + + return answer; + } + + private int getNonDuplicatedNumber(boolean[] isDuplicated) { + while(true) { + int randNum = random.nextInt(9) + 1; + if (!isDuplicated[randNum]) { + isDuplicated[randNum] = true; + return randNum; + } + } + } +} diff --git a/src/main/java/view/ErrorMessage.java b/src/main/java/view/ErrorMessage.java new file mode 100644 index 00000000..117a3ac2 --- /dev/null +++ b/src/main/java/view/ErrorMessage.java @@ -0,0 +1,21 @@ +package view; + +public enum ErrorMessage { + SYSTEM_IO_ERROR("System IO Error"), + ABNORMAL_EXIT("Abnormal Exit"), + INVALID_INPUT_NUMBER("숫자만 입력해주세요."), + INVALID_INPUT_LENGTH("3자리의 숫자를 입력해주세요."), + CONTAINS_ZERO("0이 포함된 숫자는 입력할 수 없습니다."), + NOT_POSITIVE("양수로 입력해주세요."); + + private static final String ERROR_PREFIX_MSG = "[ERROR]: "; + private final String msg; + + ErrorMessage(String msg) { + this.msg = msg; + } + + public String getMsg() { + return ERROR_PREFIX_MSG + msg; + } +} diff --git a/src/main/java/view/GameView.java b/src/main/java/view/GameView.java new file mode 100644 index 00000000..a681b015 --- /dev/null +++ b/src/main/java/view/GameView.java @@ -0,0 +1,49 @@ +package view; + +import model.GameAnswerType; + +import java.io.*; + +public class GameView { + private static final String TURN_MSG = "숫자를 입력해주세요 : "; + private static final String SINGLE_GAME_END_MSG = "3개의 숫자를 모두 맞히셨습니다! 게임 끝\n 게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\n"; + private static final String GAME_END_MSG = "게임을 완전히 종료합니다."; + + private final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); + + public void printTurnMsg() throws IOException { + bw.write(TURN_MSG); + bw.flush(); + } + + public void printResult(GameAnswerType result) throws IOException { + bw.write(result.getAnswer()); + bw.newLine(); + bw.flush(); + } + + public void printSingleGameEndMsg() throws IOException { + bw.write(SINGLE_GAME_END_MSG); + bw.flush(); + } + + public void printGameEndMsg() throws IOException { + bw.write(GAME_END_MSG); + bw.flush(); + } + + public void printErrorMsg(String message) { + try { + bw.write(message); + bw.newLine(); + bw.flush(); + } + catch (IOException e) { + System.out.println(message); + } + } + + public void releaseResource() throws IOException { + bw.close(); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..1baf16d8 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,75 @@ +package view; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class InputView { + private static final int NUMBER_LENGTH = 3; + private static final int INVALID_INPUT = -1; + private final GameView gameView; + private final BufferedReader br; + + public InputView(GameView gameView) { + this.br = new BufferedReader(new InputStreamReader(System.in)); + this.gameView = gameView; + } + + public int getUserInput() throws IOException { + while (true) { + String rawInput = br.readLine(); + int parsedInput = parseUserInput(rawInput); + + if (parsedInput != INVALID_INPUT) { + return parsedInput; + } + + gameView.printTurnMsg(); + } + } + + private int parseUserInput(String rawInput) { + if (!checkInputFormat(rawInput)) { + return INVALID_INPUT; + } + + return parseToPositiveInt(rawInput); + } + + private boolean checkInputFormat(String rawInput) { + if (!isValidLength(rawInput)) { + return false; + } + + if (rawInput.contains("0")) { + gameView.printErrorMsg(ErrorMessage.CONTAINS_ZERO.getMsg()); + return false; + } + + return true; + } + + private int parseToPositiveInt(String rawInput) { + try { + int userInput = Integer.parseInt(rawInput); + if (userInput <= 0) { + gameView.printErrorMsg(ErrorMessage.NOT_POSITIVE.getMsg()); + return INVALID_INPUT; + } + return userInput; + } + catch (NumberFormatException e) { + gameView.printErrorMsg(ErrorMessage.INVALID_INPUT_NUMBER.getMsg()); + return INVALID_INPUT; + } + + } + + private boolean isValidLength(String input) { + if (input.length() != NUMBER_LENGTH) { + gameView.printErrorMsg(ErrorMessage.INVALID_INPUT_LENGTH.getMsg()); + return false; + } + return true; + } +} diff --git a/src/test/java/model/BaseballGameNumberTest.java b/src/test/java/model/BaseballGameNumberTest.java new file mode 100644 index 00000000..fb2cffca --- /dev/null +++ b/src/test/java/model/BaseballGameNumberTest.java @@ -0,0 +1,68 @@ +package model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BaseballGameNumberTest { + + @Test + @DisplayName("3 스트라이크 체크") + void strike3() { + // given + BaseballGameNumber computer = new BaseballGameNumber(new ArrayList<>(List.of(1, 2, 3))); + BaseballGameNumber user = new BaseballGameNumber(new ArrayList<>(List.of(1, 2, 3))); + // when + GameAnswerType result = user.compare(computer); + // then + assertThat(result.getStrikeCount()).isEqualTo(3); + assertThat(result.getBallCount()).isEqualTo(0); + assertThat(result.getAnswer()).isEqualTo("3스트라이크"); + } + + @Test + @DisplayName("3볼 체크") + void ball3() { + // given + BaseballGameNumber computer = new BaseballGameNumber(new ArrayList<>(List.of(2, 3, 1))); + BaseballGameNumber user = new BaseballGameNumber(new ArrayList<>(List.of(1, 2, 3))); + // when + GameAnswerType result = user.compare(computer); + // then + assertThat(result.getStrikeCount()).isEqualTo(0); + assertThat(result.getBallCount()).isEqualTo(3); + assertThat(result.getAnswer()).isEqualTo("3볼"); + } + + @Test + @DisplayName("낫싱 체크") + void nothing() { + // given + BaseballGameNumber computer = new BaseballGameNumber(new ArrayList<>(List.of(1, 2, 3))); + BaseballGameNumber user = new BaseballGameNumber(new ArrayList<>(List.of(4, 5, 6))); + // when + GameAnswerType result = user.compare(computer); + // then + assertThat(result.getStrikeCount()).isEqualTo(0); + assertThat(result.getBallCount()).isEqualTo(0); + assertThat(result.getAnswer()).isEqualTo("낫싱"); + } + + @Test + @DisplayName("1스트라이크 1볼") + void strike1ball1() { + // given + BaseballGameNumber computer = new BaseballGameNumber(new ArrayList<>(List.of(1, 2, 3))); + BaseballGameNumber user = new BaseballGameNumber(new ArrayList<>(List.of(1, 3, 7))); + // when + GameAnswerType result = user.compare(computer); + // then + assertThat(result.getStrikeCount()).isEqualTo(1); + assertThat(result.getBallCount()).isEqualTo(1); + assertThat(result.getAnswer()).isEqualTo("1스트라이크 1볼"); + } +} diff --git a/src/test/java/model/GameAnswerTypeTest.java b/src/test/java/model/GameAnswerTypeTest.java new file mode 100644 index 00000000..9bbf1067 --- /dev/null +++ b/src/test/java/model/GameAnswerTypeTest.java @@ -0,0 +1,40 @@ +package model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GameAnswerTypeTest { + + @Test + @DisplayName("메시지 - 낫싱") + void nothingMessage() { + GameAnswerType result = new GameAnswerType(0, 0); + assertThat(result.getAnswer()).isEqualTo("낫싱"); + } + + @Test + @DisplayName("메시지 - 스트라이크") + void strikeMessage() { + GameAnswerType result = new GameAnswerType(2, 0); + assertThat(result.getAnswer()).isEqualTo("2스트라이크"); + } + + @Test + @DisplayName("메시지 - 볼") + void ballMessage() { + GameAnswerType result = new GameAnswerType(0, 2); + assertThat(result.getAnswer()).isEqualTo("2볼"); + } + + @Test + @DisplayName("메시지 - 스트라이크 & 볼") + void strikeBallMessage() { + GameAnswerType result = new GameAnswerType(2, 1); + assertThat(result.getAnswer()).isEqualTo("2스트라이크 1볼"); + } + + + +} diff --git a/src/test/java/util/IntegerParserTest.java b/src/test/java/util/IntegerParserTest.java new file mode 100644 index 00000000..7d03e253 --- /dev/null +++ b/src/test/java/util/IntegerParserTest.java @@ -0,0 +1,23 @@ +package util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IntegerParserTest { + @Test + @DisplayName("정수 리스트 변환") + void toIntArray() { + // given + int input = 456; + // when + List result = IntegerParser.toIntArray(input); + // then + assertThat(result) + .hasSize(3) + .containsExactly(4, 5, 6); + } +} diff --git a/src/test/java/util/RandomNumberGeneratorTest.java b/src/test/java/util/RandomNumberGeneratorTest.java new file mode 100644 index 00000000..87507524 --- /dev/null +++ b/src/test/java/util/RandomNumberGeneratorTest.java @@ -0,0 +1,32 @@ +package util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RandomNumberGeneratorTest { + + @RepeatedTest(50) + @DisplayName("난수 생성 시 3자리, 범위, 중복없음 체크") + void makeRand3digit() { + // given + RandomNumberGenerator generator = new RandomNumberGenerator(); + // when + int number = generator.makeRand3digit(); + String numStr = String.valueOf(number); + // then + assertThat(number).isBetween(123, 999); + assertThat(numStr).doesNotContain("0"); + + Set digitSet = new HashSet<>(); + + for (char c : numStr.toCharArray()) { + digitSet.add(c); + } + assertThat(digitSet).hasSize(3); + } +}