diff --git a/README.md b/README.md index 8d7e8aee..492f39a3 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +## 기능 목록 + +역할과 책임에 따라 **Controller, View, Domain** 세 개의 계층으로 분리되어 있습니다. + +- **Controller (`BaseballController`)** + - 게임의 전체적인 흐름(시작, 재시작, 종료)을 관리합니다. + - 사용자 입력에 따라 View와 Domain 객체 간의 상호작용을 조율합니다. + + +- **View (`InputView`, `OutputView`)** + - 사용자와의 모든 상호작용(콘솔 입출력)을 담당합니다. + - `InputView`: 사용자로부터 입력을 받습니다. + - `OutputView`: 게임 진행 상황, 결과, 오류 메시지 등을 출력합니다. + + +- **Domain (`baseball.domain.*`)** + + - **`BaseballGame`** + - 한 판의 게임 세션을 관리하며, 게임의 핵심 진행을 책임집니다. + - `Computer`를 내부적으로 생성하여 정답 번호를 가집니다. + - `playRound(Player)`: 플레이어의 추측을 받아 결과를 `Score`로 반환합니다. + + - **`Computer`** + - 서로 다른 1~9 사이의 임의의 수 3개(정답 번호)를 생성합니다. + + - **`Player`** + - 사용자가 입력한 세 자리 숫자를 보유합니다. + - **입력값 검증**: 생성 시점에서 입력값이 3자리의 숫자인지, 중복된 숫자가 없는지 등을 검증하고, 유효하지 않으면 `IllegalArgumentException`을 발생시킵니다. + + - **`Referee`** + - `Computer`의 숫자와 `Player`의 숫자를 비교하여 스트라이크와 볼의 개수를 계산하는 정적(static) 유틸리티 메소드를 제공합니다. + + - **`Score`** + - 한 라운드의 결과(스트라이크, 볼의 개수)를 저장합니다. + - `isFinished()`: 3스트라이크인지 여부를 판단하여 게임 종료 조건을 캡슐화합니다. + - `toString()`: "1볼 1스트라이크", "낫싱" 등 출력용 문자열로 변환하는 기능을 제공합니다. + + - **`GameStatus`** + - 게임 종료 후 사용자의 선택(재시작/완전 종료) 상태를 관리하는 열거형(Enum)입니다. + + diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java new file mode 100644 index 00000000..477ef655 --- /dev/null +++ b/src/main/java/baseball/Application.java @@ -0,0 +1,12 @@ +package baseball; + +import baseball.controller.BaseballController; + +public class Application { + + public static void main(String[] args) { + BaseballController controller = new BaseballController(); + controller.play(); + } + +} diff --git a/src/main/java/baseball/controller/BaseballController.java b/src/main/java/baseball/controller/BaseballController.java new file mode 100644 index 00000000..4b2f51aa --- /dev/null +++ b/src/main/java/baseball/controller/BaseballController.java @@ -0,0 +1,49 @@ +package baseball.controller; + +import baseball.domain.*; +import baseball.view.*; +import baseball.domain.BaseballGame; + +public class BaseballController { + private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + + public void play() { + do { + runGame(); + } while (isRestartRequested()); + } + + private void runGame() { + BaseballGame game = new BaseballGame(); + Score score; + do { + Player player = enrollPlayer(); + score = game.playRound(player); + outputView.printScore(score.toString()); + } while (!score.isFinished()); + + outputView.printGameEnd(); + } + + private Player enrollPlayer() { + try { + outputView.printInputRequest(); + return new Player(inputView.readNumbers()); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + return enrollPlayer(); + } + } + + private boolean isRestartRequested() { + try { + String input = inputView.readNumbers(); + GameStatus status = GameStatus.from(input); + return status.isRestart(); + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + return isRestartRequested(); + } + } +} diff --git a/src/main/java/baseball/domain/BaseballGame.java b/src/main/java/baseball/domain/BaseballGame.java new file mode 100644 index 00000000..8d398d56 --- /dev/null +++ b/src/main/java/baseball/domain/BaseballGame.java @@ -0,0 +1,17 @@ +package baseball.domain; + +import java.util.List; + +public class BaseballGame { + private final Computer computer; + + public BaseballGame() { + this.computer = new Computer(); + } + + public Score playRound(Player player) { + List computerNumbers = computer.getNumbers(); + List playerNumbers = player.getNumbers(); + return Referee.compare(computerNumbers, playerNumbers); + } +} diff --git a/src/main/java/baseball/domain/Computer.java b/src/main/java/baseball/domain/Computer.java new file mode 100644 index 00000000..224694cd --- /dev/null +++ b/src/main/java/baseball/domain/Computer.java @@ -0,0 +1,16 @@ +package baseball.domain; + +import java.util.List; +import baseball.utils.RandomNumberGenerator; + +public class Computer { + private final List numbers; + + public Computer() { + this.numbers = RandomNumberGenerator.generate(); + } + + public List getNumbers() { + return numbers; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/GameStatus.java b/src/main/java/baseball/domain/GameStatus.java new file mode 100644 index 00000000..47de9d9e --- /dev/null +++ b/src/main/java/baseball/domain/GameStatus.java @@ -0,0 +1,25 @@ +package baseball.domain; + +public enum GameStatus { + RESTART("1"), + EXIT("2"); + + private final String command; + + GameStatus(String command) { + this.command = command; + } + + public static GameStatus from(String input) { + for (GameStatus status : values()) { + if (status.command.equals(input)) { + return status; + } + } + throw new IllegalArgumentException("[ERROR] 1 또는 2만 입력 가능합니다."); + } + + public boolean isRestart() { + return this == RESTART; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/Player.java b/src/main/java/baseball/domain/Player.java new file mode 100644 index 00000000..cdc1d44a --- /dev/null +++ b/src/main/java/baseball/domain/Player.java @@ -0,0 +1,38 @@ +package baseball.domain; + +import java.util.ArrayList; +import java.util.List; + +public class Player { + private final List numbers; + + public Player(String input) { + validate(input); + this.numbers = parse(input); + } + + private void validate(String input) { + if (!input.matches("^[1-9]{3}$")) { + throw new IllegalArgumentException("[ERROR] 1-9 사이의 숫자 3개를 입력해야 합니다."); + } + if (hasDuplicate(input)) { + throw new IllegalArgumentException("[ERROR] 중복된 숫자가 있습니다."); + } + } + + private boolean hasDuplicate(String input) { + return input.chars().distinct().count() != input.length(); + } + + private List parse(String input) { + List list = new ArrayList<>(); + for (char c : input.toCharArray()) { + list.add(Character.getNumericValue(c)); + } + return list; + } + + public List getNumbers() { + return numbers; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/Referee.java b/src/main/java/baseball/domain/Referee.java new file mode 100644 index 00000000..070d4303 --- /dev/null +++ b/src/main/java/baseball/domain/Referee.java @@ -0,0 +1,29 @@ +package baseball.domain; + +import java.util.List; +import java.util.Objects; + +public class Referee { + public static Score compare(List computer, List player) { + int strike = 0; + int ball = 0; + + for (int i = 0; i < 3; i++) { + strike += countStrike(computer.get(i), player.get(i)); + ball += countBall(computer, player, i); + } + return new Score(strike, ball); + } + + private static int countStrike(int computerNum, int playerNum) { + if (computerNum == playerNum) return 1; + return 0; + } + + private static int countBall(List computer, List player, int index) { + if (computer.contains(player.get(index)) && !Objects.equals(computer.get(index), player.get(index))) { + return 1; + } + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/domain/Score.java b/src/main/java/baseball/domain/Score.java new file mode 100644 index 00000000..ec573a50 --- /dev/null +++ b/src/main/java/baseball/domain/Score.java @@ -0,0 +1,27 @@ +package baseball.domain; + +public class Score { + private final int strike; + private final int ball; + + public Score(int strike, int ball) { + this.strike = strike; + this.ball = ball; + } + + public int getStrike() { + return strike; + } + + public boolean isFinished() { + return strike == 3; + } + + @Override + public String toString() { + if (strike == 0 && ball == 0) return "낫싱"; + if (strike == 0) return ball + "볼"; + if (ball == 0) return strike + "스트라이크"; + return ball + "볼 " + strike + "스트라이크"; + } +} \ No newline at end of file diff --git a/src/main/java/baseball/utils/RandomNumberGenerator.java b/src/main/java/baseball/utils/RandomNumberGenerator.java new file mode 100644 index 00000000..b9f23f7d --- /dev/null +++ b/src/main/java/baseball/utils/RandomNumberGenerator.java @@ -0,0 +1,25 @@ +package baseball.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class RandomNumberGenerator { + + public static List generate() { + List numbers = new ArrayList<>(); + Random random = new Random(); + + while (numbers.size() < 3) { + int randomNumber = random.nextInt(9) + 1; + addIfUnique(numbers, randomNumber); + } + return numbers; + } + + private static void addIfUnique(List list, int number) { + if (!list.contains(number)) { + list.add(number); + } + } +} \ No newline at end of file diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 00000000..27c3e64b --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,13 @@ +package baseball.view; + +import java.util.Scanner; + +public class InputView { + + Scanner scanner = new Scanner(System.in); + + public String readNumbers() { + + return scanner.nextLine(); + } +} diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 00000000..c987f8fa --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,21 @@ +package baseball.view; + +public class OutputView { + public void printError(String message) { + System.out.println(message); + } + + public void printInputRequest() { + System.out.print("숫자를 입력해 주세요 : "); + } + + public void printScore(String string) { + System.out.println(string); + } + + public void printGameEnd() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝\n" + + "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/baseball/domain/BaseballGameTest.java b/src/test/java/baseball/domain/BaseballGameTest.java new file mode 100644 index 00000000..deedb759 --- /dev/null +++ b/src/test/java/baseball/domain/BaseballGameTest.java @@ -0,0 +1,20 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class BaseballGameTest { + + @Test + @DisplayName("playRound는 플레이어의 숫자를 기반으로 정확한 Score를 반환해야 한다.") + void playRoundShouldReturnCorrectScore() { + BaseballGame game = new BaseballGame(); + Player player = new Player("123"); + + Score score = game.playRound(player); + + assertThat(score).isNotNull(); + } +} diff --git a/src/test/java/baseball/domain/ComputerTest.java b/src/test/java/baseball/domain/ComputerTest.java new file mode 100644 index 00000000..2859905c --- /dev/null +++ b/src/test/java/baseball/domain/ComputerTest.java @@ -0,0 +1,23 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ComputerTest { + + @RepeatedTest(100) + @DisplayName("컴퓨터는 3자리의 중복되지 않는 숫자를 생성한다.") + void createComputerShouldGenerateThreeUniqueNumbers() { + + Computer computer = new Computer(); + List numbers = computer.getNumbers(); + + assertThat(numbers).hasSize(3); + assertThat(numbers).doesNotHaveDuplicates(); + } +} diff --git a/src/test/java/baseball/domain/GameStatusTest.java b/src/test/java/baseball/domain/GameStatusTest.java new file mode 100644 index 00000000..0ac2708d --- /dev/null +++ b/src/test/java/baseball/domain/GameStatusTest.java @@ -0,0 +1,53 @@ +package baseball.domain; + +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.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GameStatusTest { + + @Test + @DisplayName("'1' 입력 시 RESTART 상태를 반환한다.") + void fromWithOneShouldReturnRestart() { + GameStatus status = GameStatus.from("1"); + + assertThat(status).isEqualTo(GameStatus.RESTART); + } + + @Test + @DisplayName("'2' 입력 시 EXIT 상태를 반환한다.") + void fromWithTwoShouldReturnExit() { + GameStatus status = GameStatus.from("2"); + + assertThat(status).isEqualTo(GameStatus.EXIT); + } + + @ParameterizedTest + @ValueSource(strings = {"3", "a", "12"}) + @DisplayName("'1' 또는 '2'가 아닌 입력 시 예외가 발생한다.") + void fromWithInvalidInputShouldThrowException(String input) { + assertThatThrownBy(() -> GameStatus.from(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 1 또는 2만 입력 가능합니다."); + } + + @Test + @DisplayName("RESTART 상태는 isRestart() 호출 시 true를 반환한다.") + void isRestartForRestartStatusShouldReturnTrue() { + GameStatus status = GameStatus.RESTART; + + assertThat(status.isRestart()).isTrue(); + } + + @Test + @DisplayName("EXIT 상태는 isRestart() 호출 시 false를 반환한다.") + void isRestartForExitStatusShouldReturnFalse() { + GameStatus status = GameStatus.EXIT; + + assertThat(status.isRestart()).isFalse(); + } +} diff --git a/src/test/java/baseball/domain/PlayerTest.java b/src/test/java/baseball/domain/PlayerTest.java new file mode 100644 index 00000000..fc8abdce --- /dev/null +++ b/src/test/java/baseball/domain/PlayerTest.java @@ -0,0 +1,54 @@ +package baseball.domain; + +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PlayerTest { + + @Test + @DisplayName("플레이어는 3자리의 유효한 숫자로 생성된다.") + void createPlayerSuccess() { + String input = "123"; + + Player player = new Player(input); + List numbers = player.getNumbers(); + + assertThat(numbers).containsExactly(1, 2, 3); + } + + @ParameterizedTest + @ValueSource(strings = {"12", "1234", "abc"}) + @DisplayName("플레이어 생성 시 3자리의 1-9 숫자가 아니면 예외가 발생한다.") + void createPlayerWithInvalidFormatShouldThrowException(String input) { + assertThatThrownBy(() -> new Player(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 1-9 사이의 숫자 3개를 입력해야 합니다."); + } + + @Test + @DisplayName("플레이어 생성 시 중복된 숫자가 있으면 예외가 발생한다.") + void createPlayerWithDuplicateNumbersShouldThrowException() { + String input = "112"; + + assertThatThrownBy(() -> new Player(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 중복된 숫자가 있습니다."); + } + + @Test + @DisplayName("플레이어 생성 시 0이 포함되어 있으면 예외가 발생한다.") + void createPlayerWithZeroShouldThrowException() { + String input = "102"; + + assertThatThrownBy(() -> new Player(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 1-9 사이의 숫자 3개를 입력해야 합니다."); + } +} diff --git a/src/test/java/baseball/domain/RefereeTest.java b/src/test/java/baseball/domain/RefereeTest.java new file mode 100644 index 00000000..fa720eb6 --- /dev/null +++ b/src/test/java/baseball/domain/RefereeTest.java @@ -0,0 +1,83 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RefereeTest { + + @Test + @DisplayName("3스트라이크를 정확히 판정한다.") + void compare3Strikes() { + List computer = List.of(1, 2, 3); + List player = List.of(1, 2, 3); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(3); + assertThat(score.toString()).isEqualTo("3스트라이크"); + } + + @Test + @DisplayName("1스트라이크 2볼을 정확히 판정한다.") + void compare1Strike2Balls() { + List computer = List.of(1, 2, 3); + List player = List.of(1, 3, 2); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(1); + assertThat(score.toString()).isEqualTo("2볼 1스트라이크"); + } + + @Test + @DisplayName("3볼을 정확히 판정한다.") + void compare3Balls() { + List computer = List.of(1, 2, 3); + List player = List.of(3, 1, 2); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(0); + assertThat(score.toString()).isEqualTo("3볼"); + } + + @Test + @DisplayName("낫싱을 정확히 판정한다.") + void compareNothing() { + List computer = List.of(1, 2, 3); + List player = List.of(4, 5, 6); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(0); + assertThat(score.toString()).isEqualTo("낫싱"); + } + + @Test + @DisplayName("1스트라이크를 정확히 판정한다.") + void compare1Strike() { + List computer = List.of(1, 2, 3); + List player = List.of(1, 4, 5); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(1); + assertThat(score.toString()).isEqualTo("1스트라이크"); + } + + @Test + @DisplayName("1볼을 정확히 판정한다.") + void compare1Ball() { + List computer = List.of(1, 2, 3); + List player = List.of(4, 1, 5); + + Score score = Referee.compare(computer, player); + + assertThat(score.getStrike()).isEqualTo(0); + assertThat(score.toString()).isEqualTo("1볼"); + } +} diff --git a/src/test/java/baseball/domain/ScoreTest.java b/src/test/java/baseball/domain/ScoreTest.java new file mode 100644 index 00000000..fa050b7b --- /dev/null +++ b/src/test/java/baseball/domain/ScoreTest.java @@ -0,0 +1,69 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScoreTest { + + @Test + @DisplayName("스트라이크와 볼이 모두 있을 때, 'n볼 m스트라이크' 형식으로 반환한다.") + void toStringWithStrikesAndBalls() { + Score score = new Score(1, 2); + + String result = score.toString(); + + assertThat(result).isEqualTo("2볼 1스트라이크"); + } + + @Test + @DisplayName("스트라이크만 있을 때, 'n스트라이크' 형식으로 반환한다.") + void toStringWithOnlyStrikes() { + Score score = new Score(2, 0); + + String result = score.toString(); + + assertThat(result).isEqualTo("2스트라이크"); + } + + @Test + @DisplayName("볼만 있을 때, 'n볼' 형식으로 반환한다.") + void toStringWithOnlyBalls() { + Score score = new Score(0, 2); + + String result = score.toString(); + + assertThat(result).isEqualTo("2볼"); + } + + @Test + @DisplayName("스트라이크와 볼이 모두 없을 때, '낫싱'을 반환한다.") + void toStringWithNothing() { + Score score = new Score(0, 0); + + String result = score.toString(); + + assertThat(result).isEqualTo("낫싱"); + } + + @Test + @DisplayName("3스트라이크일 때 isFinished는 true를 반환한다.") + void isFinishedWhen3StrikesShouldReturnTrue() { + Score score = new Score(3, 0); + + boolean finished = score.isFinished(); + + assertThat(finished).isTrue(); + } + + @Test + @DisplayName("3스트라이크가 아닐 때 isFinished는 false를 반환한다.") + void isFinishedWhenNot3StrikesShouldReturnFalse() { + Score score = new Score(2, 1); + + boolean finished = score.isFinished(); + + assertThat(finished).isFalse(); + } +}