diff --git a/README.md b/README.md index fcf3f057..7b235a61 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,24 @@ ## 과제 제출 과정 * [과제 제출 방법](https://github.com/next-step/nextstep-docs/tree/master/ent-precourse) + +## 구현할 기능 +- 상대방이 무작위 3개의 숫자 선택 + - 선택한 3개의 숫자는 서로 달라야한다. +- 플레이어로부터 입력을 받는다. + - 입력과 정답은 같은 갯수의 숫자로 이루어져야한다. + - 입력 받은 숫자들은 서로 달라야한다. +- 플레이어의 입력과 정답으로부터 스코어를 계산한다. + - 스코어 계산은 입력의 각 자리수마다 따로 계산한다. + - 입력 받은 수가 정답과 같은 자리에 있으면 스트라이크(STRIKE)로 계산한다. + - 입력 받은 수가 정답에 존재하지만 다른 자리에 있으면 볼(BALL)로 계산한다. + - 입력 받은 수가 정답에 존재하지 않으면 낫싱(NOTHING)으로 계산한다. +- 플레이어가 입력한 숫자에 대한 결과를 출력한다. 스트라이크가 `s`개, 볼이 `b`개인 경우 출력은 다음과 같다. + - `s >= 1, b >= 1` + - `{s} 스트라이크 {b} 볼` 출력 + - `s >= 1, b == 0` + - `{s} 스트라이크` 출력 + - `s == 0, b >= 1` + - `{b} 볼` 출력 + - `s == 0, b == 0` + - `낫싱` 출력 \ No newline at end of file diff --git a/src/main/java/MainApplication.java b/src/main/java/MainApplication.java new file mode 100644 index 00000000..64ca1c2a --- /dev/null +++ b/src/main/java/MainApplication.java @@ -0,0 +1,9 @@ +import player.Player; + +public class MainApplication { + + public static void main(String[] args) { + Player player = new Player(); + player.start(); + } +} diff --git a/src/main/java/exception/GameCommandNoutFoundException.java b/src/main/java/exception/GameCommandNoutFoundException.java new file mode 100644 index 00000000..46af5a94 --- /dev/null +++ b/src/main/java/exception/GameCommandNoutFoundException.java @@ -0,0 +1,10 @@ +package exception; + +public class GameCommandNoutFoundException extends RuntimeException { + + public static final String GAME_COMMAND_NOT_FOUND_EXCEPTION = "잘못된 커멘드 입력입니다."; + + public GameCommandNoutFoundException() { + super(GAME_COMMAND_NOT_FOUND_EXCEPTION); + } +} diff --git a/src/main/java/exception/InputLengthValidationException.java b/src/main/java/exception/InputLengthValidationException.java new file mode 100644 index 00000000..eb6295fc --- /dev/null +++ b/src/main/java/exception/InputLengthValidationException.java @@ -0,0 +1,10 @@ +package exception; + +public class InputLengthValidationException extends RuntimeException { + + public static final String INPUT_LENGTH_VALIDATION_EXCEPTION_MESSAGE = "잘못된 길이의 입력입니다."; + + public InputLengthValidationException() { + super(INPUT_LENGTH_VALIDATION_EXCEPTION_MESSAGE); + } +} diff --git a/src/main/java/exception/InputNumberDuplicationException.java b/src/main/java/exception/InputNumberDuplicationException.java new file mode 100644 index 00000000..84003b3b --- /dev/null +++ b/src/main/java/exception/InputNumberDuplicationException.java @@ -0,0 +1,10 @@ +package exception; + +public class InputNumberDuplicationException extends RuntimeException { + + public static final String INPUT_NUMBER_DUPLICATION_EXCEPTION_MESSAGE = "입력에는 중복된 숫자가 존재할 수 없습니다."; + + public InputNumberDuplicationException() { + super(INPUT_NUMBER_DUPLICATION_EXCEPTION_MESSAGE); + } +} diff --git a/src/main/java/game/BaseBallGame.java b/src/main/java/game/BaseBallGame.java new file mode 100644 index 00000000..ad51f46d --- /dev/null +++ b/src/main/java/game/BaseBallGame.java @@ -0,0 +1,43 @@ +package game; + +import opponent.Opponent; +import score.Score; +import settings.GameSetting; +import ui.InputManager; +import ui.OutputManager; + +import java.util.List; + +public class BaseBallGame { + + private final Opponent opponent; + + public BaseBallGame(Opponent opponent) { + this.opponent = opponent; + } + + public void start() { + Score score; + do { + score = nextStage(); + } while (!isGameOver(score)); + GameSetting.getInstance().getOutputManager().printGameOverMessage(); + } + + private boolean isGameOver(Score score) { + return score.getStrikeCount() == 3; + } + + private Score nextStage() { + Score score = new Score(0, 0); + OutputManager outputManager = GameSetting.getInstance().getOutputManager(); + InputManager inputManager = GameSetting.getInstance().getInputManager(); + outputManager.printInputMessage(); + List inputNumbers = inputManager.getInputNumbers(); + for (int i = 0; i < inputNumbers.size(); i++) { + score.updateScore(opponent.getAnswer(), inputNumbers.get(i), i); + } + outputManager.printResult(score); + return score; + } +} diff --git a/src/main/java/game/GameCommand.java b/src/main/java/game/GameCommand.java new file mode 100644 index 00000000..8cc63e09 --- /dev/null +++ b/src/main/java/game/GameCommand.java @@ -0,0 +1,41 @@ +package game; + +import exception.GameCommandNoutFoundException; + +import java.util.HashMap; +import java.util.Map; + +public enum GameCommand { + + RESTART(1), + END(2); + + private static final Map gameCommandMapping = new HashMap<>(); + + static { + for (GameCommand gameCommand : GameCommand.values()) { + gameCommandMapping.put( + gameCommand.getCommandNumber(), + gameCommand + ); + } + } + + private final int commandNumber; + + GameCommand(int commandNumber) { + this.commandNumber = commandNumber; + } + + public static GameCommand getCommand(int commandNumber) { + if (!gameCommandMapping.containsKey(commandNumber)) { + throw new GameCommandNoutFoundException(); + } + + return gameCommandMapping.get(commandNumber); + } + + public int getCommandNumber() { + return commandNumber; + } +} diff --git a/src/main/java/input/InputParser.java b/src/main/java/input/InputParser.java new file mode 100644 index 00000000..a5f8b244 --- /dev/null +++ b/src/main/java/input/InputParser.java @@ -0,0 +1,15 @@ +package input; + +import java.util.ArrayList; +import java.util.List; + +public class InputParser { + + public List toIntegerList(String input) throws NumberFormatException { + ArrayList result = new ArrayList<>(); + for (String element : input.split("")) { + result.add(Integer.parseInt(element)); + } + return result; + } +} diff --git a/src/main/java/input/InputValidator.java b/src/main/java/input/InputValidator.java new file mode 100644 index 00000000..91a2522d --- /dev/null +++ b/src/main/java/input/InputValidator.java @@ -0,0 +1,27 @@ +package input; + +import exception.InputLengthValidationException; +import exception.InputNumberDuplicationException; + +import java.util.HashSet; +import java.util.List; + +public class InputValidator { + + public void validateInput(List input) { + if (!isValidSize(input)) { + throw new InputLengthValidationException(); + } + if (hasDuplicatedNumbers(input)) { + throw new InputNumberDuplicationException(); + } + } + + private boolean isValidSize(List input) { + return input.size() == 3; + } + + private boolean hasDuplicatedNumbers(List input) { + return new HashSet<>(input).size() != input.size(); + } +} diff --git a/src/main/java/opponent/Opponent.java b/src/main/java/opponent/Opponent.java new file mode 100644 index 00000000..9f2fec5c --- /dev/null +++ b/src/main/java/opponent/Opponent.java @@ -0,0 +1,27 @@ +package opponent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Opponent { + private final List answer; + + public Opponent() { + answer = Collections.unmodifiableList(chooseAnswer()); + } + + public List getAnswer() { + return answer; + } + + protected List chooseAnswer() { + ArrayList possibleNumbers = new ArrayList<>(); + for (int i = 1; i < 10; i++) { + possibleNumbers.add(i); + } + Collections.shuffle(possibleNumbers); + return possibleNumbers.subList(0, 3); + } + +} diff --git a/src/main/java/player/Player.java b/src/main/java/player/Player.java new file mode 100644 index 00000000..d2a013c2 --- /dev/null +++ b/src/main/java/player/Player.java @@ -0,0 +1,25 @@ +package player; + +import game.BaseBallGame; +import game.GameCommand; +import opponent.Opponent; +import settings.GameSetting; +import ui.InputManager; + +public class Player { + + public void start() { + InputManager inputManager = GameSetting.getInstance().getInputManager(); + do { + playRound(); + } while (inputManager.getInputGameCommand() == GameCommand.RESTART); + } + + protected void playRound() { + Opponent opponent = new Opponent(); + BaseBallGame baseBallGame = new BaseBallGame(opponent); + baseBallGame.start(); + } + + +} diff --git a/src/main/java/score/BaseBallCriterion.java b/src/main/java/score/BaseBallCriterion.java new file mode 100644 index 00000000..9a6ec4a6 --- /dev/null +++ b/src/main/java/score/BaseBallCriterion.java @@ -0,0 +1,9 @@ +package score; + +import java.util.List; + +@FunctionalInterface +public interface BaseBallCriterion { + + boolean judge(List answer, Integer userNumber, Integer pos); +} diff --git a/src/main/java/score/BaseBallJudgement.java b/src/main/java/score/BaseBallJudgement.java new file mode 100644 index 00000000..850ac61a --- /dev/null +++ b/src/main/java/score/BaseBallJudgement.java @@ -0,0 +1,25 @@ +package score; + +import java.util.List; + +public enum BaseBallJudgement { + + STRIKE((answer, userNumber, pos) -> answer.get(pos).equals(userNumber)), + BALL((answer, userNumber, pos) -> { + if (STRIKE.hit(answer, userNumber, pos)) { + return false; + } + return answer.contains(userNumber); + }), + NOTHING((answer, userNumber, pos) -> !answer.contains(userNumber)); + + private final BaseBallCriterion criterion; + + BaseBallJudgement(BaseBallCriterion criterion) { + this.criterion = criterion; + } + + public boolean hit(List answer, Integer userNumber, Integer pos) { + return criterion.judge(answer, userNumber, pos); + } +} diff --git a/src/main/java/score/Score.java b/src/main/java/score/Score.java new file mode 100644 index 00000000..39a91c66 --- /dev/null +++ b/src/main/java/score/Score.java @@ -0,0 +1,51 @@ +package score; + +import java.util.List; +import java.util.Objects; + +import static score.BaseBallJudgement.BALL; +import static score.BaseBallJudgement.STRIKE; + +public class Score { + + private Integer strikeCount; + + private Integer ballCount; + + public Score(Integer strikeCount, Integer ballCount) { + this.strikeCount = strikeCount; + this.ballCount = ballCount; + } + + public Integer getStrikeCount() { + return strikeCount; + } + + public Integer getBallCount() { + return ballCount; + } + + public void updateScore(List answer, Integer userNumber, Integer pos) { + if (STRIKE.hit(answer, userNumber, pos)) { + strikeCount += 1; + return; + } + if (BALL.hit(answer, userNumber, pos)) { + ballCount += 1; + return; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Score score = (Score) o; + return Objects.equals(strikeCount, score.strikeCount) && Objects.equals(ballCount, score.ballCount); + } + + @Override + public int hashCode() { + return Objects.hash(strikeCount, ballCount); + } +} diff --git a/src/main/java/settings/GameSetting.java b/src/main/java/settings/GameSetting.java new file mode 100644 index 00000000..4864e22b --- /dev/null +++ b/src/main/java/settings/GameSetting.java @@ -0,0 +1,32 @@ +package settings; + +import ui.InputManager; +import ui.OutputManager; + +public class GameSetting { + + private final InputManager inputManager; + + private final OutputManager outputManager; + + private GameSetting() { + inputManager = new InputManager(System.in); + outputManager = new OutputManager(System.out); + } + + public static GameSetting getInstance() { + return GameSettingHolder.GAME_SETTING; + } + + public InputManager getInputManager() { + return inputManager; + } + + public OutputManager getOutputManager() { + return outputManager; + } + + private static class GameSettingHolder { + private static final GameSetting GAME_SETTING = new GameSetting(); + } +} diff --git a/src/main/java/ui/InputManager.java b/src/main/java/ui/InputManager.java new file mode 100644 index 00000000..3da1448b --- /dev/null +++ b/src/main/java/ui/InputManager.java @@ -0,0 +1,41 @@ +package ui; + +import game.GameCommand; +import input.InputParser; +import input.InputValidator; + +import java.io.InputStream; +import java.util.List; +import java.util.Scanner; + +public class InputManager { + + private final Scanner sc; + private final InputValidator inputValidator; + private final InputParser inputParser; + + public InputManager() { + sc = new Scanner(System.in); + inputValidator = new InputValidator(); + inputParser = new InputParser(); + } + + public InputManager(InputStream inputStream) { + sc = new Scanner(inputStream); + inputValidator = new InputValidator(); + inputParser = new InputParser(); + } + + public List getInputNumbers() { + String inputString = sc.next(); + List input = inputParser.toIntegerList(inputString); + inputValidator.validateInput(input); + + return input; + } + + public GameCommand getInputGameCommand() { + int gameCommand = sc.nextInt(); + return GameCommand.getCommand(gameCommand); + } +} diff --git a/src/main/java/ui/OutputManager.java b/src/main/java/ui/OutputManager.java new file mode 100644 index 00000000..f7763418 --- /dev/null +++ b/src/main/java/ui/OutputManager.java @@ -0,0 +1,51 @@ +package ui; + +import score.Score; + +import java.io.OutputStream; +import java.io.PrintWriter; + +public class OutputManager { + private final PrintWriter printWriter; + + public OutputManager(OutputStream os) { + printWriter = new PrintWriter(os); + } + + public void printInputMessage() { + printWriter.print("숫자를 입력해주세요 : "); + printWriter.flush(); + } + + public void printGameOverMessage() { + printWriter.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료"); + printWriter.println("게임을 새로 시작하려면 1, 종료하려 2를 입력하세요."); + printWriter.flush(); + } + + public void printResult(Score score) { + if (score.getStrikeCount() == 0 && score.getBallCount() == 0) { + printWriter.println("낫싱"); + printWriter.flush(); + return; + } + String strikeResultMessage = strikeCountToString(score.getStrikeCount()); + String ballResultMessage = ballCountToString(score.getBallCount()); + printWriter.println(strikeResultMessage + ballResultMessage); + printWriter.flush(); + } + + private String strikeCountToString(int strikeCount) { + if (strikeCount == 0) { + return ""; + } + return String.format("%d 스트라이크 ", strikeCount); + } + + private String ballCountToString(int ballCount) { + if (ballCount == 0) { + return ""; + } + return String.format("%d 볼 ", ballCount); + } +} diff --git a/src/test/java/input/InputParserTest.java b/src/test/java/input/InputParserTest.java new file mode 100644 index 00000000..3897c823 --- /dev/null +++ b/src/test/java/input/InputParserTest.java @@ -0,0 +1,25 @@ +package input; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class InputParserTest { + + InputParser inputParser = new InputParser(); + + @ParameterizedTest + @ValueSource(strings = {"a123", "abc", "12a34"}) + void toIntegerList_숫자가_아닌_입력시_예외_발생(String input) { + assertThatExceptionOfType(NumberFormatException.class) + .isThrownBy(() -> inputParser.toIntegerList(input)); + } + + @ParameterizedTest + @ValueSource(strings = {"123", "1234", "1231241"}) + void toIntegerList_숫자만_있는_입력시_정상_실행(String input) { + assertDoesNotThrow(() -> inputParser.toIntegerList(input)); + } +} diff --git a/src/test/java/input/InputValidatorTest.java b/src/test/java/input/InputValidatorTest.java new file mode 100644 index 00000000..216865dc --- /dev/null +++ b/src/test/java/input/InputValidatorTest.java @@ -0,0 +1,80 @@ +package input; + +import exception.InputLengthValidationException; +import exception.InputNumberDuplicationException; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class InputValidatorTest { + + InputValidator inputValidator = new InputValidator(); + + @ParameterizedTest + @MethodSource("prepareInputOfInvalidSize") + void validateInput_입력_길이가_올바르지_않으면_예외_발생(List input) { + assertThatExceptionOfType(InputLengthValidationException.class) + .isThrownBy(() -> inputValidator.validateInput(input)) + .withMessage(InputLengthValidationException.INPUT_LENGTH_VALIDATION_EXCEPTION_MESSAGE); + } + + Stream prepareInputOfInvalidSize() { + return Stream.of( + Arguments.of(List.of(1)), + Arguments.of(List.of(1, 2)), + Arguments.of(List.of(1, 2, 3, 4)) + ); + } + + @ParameterizedTest + @MethodSource("prepareInputOfValidSize") + void validateInput_입력_길이가_올바르면_정상_실행(List input) { + assertDoesNotThrow(() -> inputValidator.validateInput(input)); + } + + Stream prepareInputOfValidSize() { + return Stream.of( + Arguments.of(List.of(1, 2, 3)), + Arguments.of(List.of(2, 3, 4)), + Arguments.of(List.of(4, 5, 6)) + ); + } + + @ParameterizedTest + @MethodSource("prepareInputHavingDuplicatedNumbers") + void validateInput_입력에_중복된_숫자가_존재하면_예외_발생(List input) { + assertThatExceptionOfType(InputNumberDuplicationException.class) + .isThrownBy(() -> inputValidator.validateInput(input)) + .withMessage(InputNumberDuplicationException.INPUT_NUMBER_DUPLICATION_EXCEPTION_MESSAGE); + } + + Stream prepareInputHavingDuplicatedNumbers() { + return Stream.of( + Arguments.of(List.of(1, 1, 1)), + Arguments.of(List.of(1, 1, 2)), + Arguments.of(List.of(1, 4, 4)) + ); + } + + @ParameterizedTest + @MethodSource("prepareInputNotHavingDuplicatedNumbers") + void validateInput_입력에_중복된_숫자가_없으면_정상_실행(List input) { + assertDoesNotThrow(() -> inputValidator.validateInput(input)); + } + + Stream prepareInputNotHavingDuplicatedNumbers() { + return Stream.of( + Arguments.of(List.of(1, 2, 3)), + Arguments.of(List.of(1, 4, 5)), + Arguments.of(List.of(2, 3, 4)) + ); + } +} diff --git a/src/test/java/opponent/OpponentTest.java b/src/test/java/opponent/OpponentTest.java new file mode 100644 index 00000000..a1f0ee13 --- /dev/null +++ b/src/test/java/opponent/OpponentTest.java @@ -0,0 +1,34 @@ +package opponent; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; + +class OpponentTest { + + Opponent opponent = new Opponent(); + + @Test + @DisplayName("chooseAnswer로 반환된 리스트는 크기가 3이어야 한다.") + void chooseAnswer는_크기가_3인_리스트를_반환한다() { + // given when + List answer = opponent.chooseAnswer(); + + // then + Assertions.assertThat(answer).hasSize(3); + } + + @Test + @DisplayName("chooseAnswer로 반환된 리스트는 서로 다른 숫자로 이루어져 있다.") + void chooseAnswer는_중복되지_않는_숫자로_이루어진_리스트를_반환한다() { + // given when + List answer = opponent.chooseAnswer(); + HashSet answerSet = new HashSet<>(answer); + + // then + Assertions.assertThat(answer).hasSameSizeAs(answerSet); + } +} \ No newline at end of file diff --git a/src/test/java/score/BaseBallJudgementTest.java b/src/test/java/score/BaseBallJudgementTest.java new file mode 100644 index 00000000..2fa549df --- /dev/null +++ b/src/test/java/score/BaseBallJudgementTest.java @@ -0,0 +1,99 @@ +package score; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static score.BaseBallJudgement.*; + +class BaseBallJudgementTest { + + static Stream prepareStrikeTrueData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 1, 0), + Arguments.of(List.of(1, 2, 3), 2, 1), + Arguments.of(List.of(1, 2, 3), 3, 2) + ); + } + + static Stream prepareStrikeFalseData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 2, 0), + Arguments.of(List.of(1, 2, 3), 2, 2), + Arguments.of(List.of(1, 2, 3), 3, 1) + ); + } + + static Stream prepareBallTrueData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 2, 0), + Arguments.of(List.of(1, 2, 3), 2, 2), + Arguments.of(List.of(1, 2, 3), 3, 1) + ); + } + + static Stream prepareBallFalseData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 2, 1), + Arguments.of(List.of(1, 2, 3), 4, 2), + Arguments.of(List.of(1, 2, 3), 5, 1) + ); + } + + static Stream prepareNothingTrueData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 4, 0), + Arguments.of(List.of(1, 2, 3), 5, 2), + Arguments.of(List.of(1, 2, 3), 6, 1) + ); + } + + static Stream prepareNothingFalseData() { + return Stream.of( + Arguments.of(List.of(1, 2, 3), 2, 0), + Arguments.of(List.of(1, 2, 3), 2, 2), + Arguments.of(List.of(1, 2, 3), 3, 1) + ); + } + + @ParameterizedTest + @MethodSource("prepareStrikeTrueData") + void STRIKE_같은_위치에_같은_숫자가_있으면_스트라이크_true(List answer, Integer userNum, Integer pos) { + assertTrue(STRIKE.hit(answer, userNum, pos)); + } + + @ParameterizedTest + @MethodSource("prepareStrikeFalseData") + void STRIKE_같은_위치에_같은_숫자가_있으면_스트라이크_false(List answer, Integer userNum, Integer pos) { + assertFalse(STRIKE.hit(answer, userNum, pos)); + } + + @ParameterizedTest + @MethodSource("prepareBallTrueData") + void BALL_다른_위치에_같은_숫자가_있으면_볼_true(List answer, Integer userNum, Integer pos) { + assertTrue(BALL.hit(answer, userNum, pos)); + } + + @ParameterizedTest + @MethodSource("prepareBallFalseData") + void BALL_다른_위치에_같은_숫자가_있으면_볼_false(List answer, Integer userNum, Integer pos) { + assertFalse(BALL.hit(answer, userNum, pos)); + } + + @ParameterizedTest + @MethodSource("prepareNothingTrueData") + void NOTHING_입력한_숫자가_존재하지_않으면_낫싱_true(List answer, Integer userNum, Integer pos) { + assertTrue(NOTHING.hit(answer, userNum, pos)); + } + + @ParameterizedTest + @MethodSource("prepareNothingFalseData") + void NOTHING_입력한_숫자가_존재하지_않으면_낫싱_false(List answer, Integer userNum, Integer pos) { + assertFalse(NOTHING.hit(answer, userNum, pos)); + } +}