diff --git a/build.gradle b/build.gradle index 8172fb73f..fd5481180 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'eclipse' group = 'camp.nextstep' version = '1.0.0' -sourceCompatibility = '1.8' +sourceCompatibility = '11' repositories { mavenCentral() diff --git a/src/main/java/.gitkeep b/src/main/java/baseball/.gitkeep similarity index 100% rename from src/main/java/.gitkeep rename to src/main/java/baseball/.gitkeep diff --git a/src/main/java/baseball/Main.java b/src/main/java/baseball/Main.java new file mode 100644 index 000000000..88c0eb85f --- /dev/null +++ b/src/main/java/baseball/Main.java @@ -0,0 +1,30 @@ +package baseball; + +import baseball.controller.GameController; +import baseball.controller.RoundController; +import baseball.domain.Generation; +import baseball.domain.RandomNumberGeneration; +import baseball.view.GameConsoleView; +import baseball.view.GameView; +import baseball.view.RoundInputConsoleView; +import baseball.view.RoundInputView; +import baseball.view.RoundOutputConsoleView; +import baseball.view.RoundOutputView; + +import java.util.Random; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + RoundInputView roundInputView = RoundInputConsoleView.from(scanner); + GameView gameView = GameConsoleView.from(scanner); + RoundOutputView roundOutputView = RoundOutputConsoleView.getInstance(); + Random random = new Random(); + Generation generation = new RandomNumberGeneration(random); + RoundController roundController = RoundController.of(roundInputView, roundOutputView, generation); + GameController gameController = GameController.of(roundController, gameView); + gameController.run(); + scanner.close(); + } +} diff --git a/src/main/java/baseball/controller/GameController.java b/src/main/java/baseball/controller/GameController.java new file mode 100644 index 000000000..5d11ed0e0 --- /dev/null +++ b/src/main/java/baseball/controller/GameController.java @@ -0,0 +1,28 @@ +package baseball.controller; + +import baseball.domain.GameStatus; +import baseball.view.GameView; + +public class GameController { + + private final GameView gameView; + private final RoundController roundController; + + public GameController(final RoundController roundController, final GameView gameView) { + this.roundController = roundController; + this.gameView = gameView; + } + + public static GameController of(final RoundController roundController, final GameView gameView) { + return new GameController(roundController, gameView); + } + + public void run() { + GameStatus gameStatus = GameStatus.PLAY; + + while (gameStatus == GameStatus.PLAY) { + roundController.run(); + gameStatus = gameView.gameInput(); + } + } +} diff --git a/src/main/java/baseball/controller/RoundController.java b/src/main/java/baseball/controller/RoundController.java new file mode 100644 index 000000000..69d30ca5e --- /dev/null +++ b/src/main/java/baseball/controller/RoundController.java @@ -0,0 +1,47 @@ +package baseball.controller; + +import baseball.domain.Balls; +import baseball.domain.Generation; +import baseball.domain.Round; +import baseball.dto.RoundInputDto; +import baseball.dto.RoundOutputDto; +import baseball.view.RoundInputView; +import baseball.view.RoundOutputView; + +import java.util.List; + +public class RoundController { + + private final RoundInputView roundInputView; + private final RoundOutputView roundOutputView; + private final Generation generation; + + private RoundController(final RoundInputView roundInputView, final RoundOutputView roundOutputView, + final Generation generation) { + this.roundInputView = roundInputView; + this.roundOutputView = roundOutputView; + this.generation = generation; + } + + public static RoundController of(final RoundInputView roundInputView, final RoundOutputView roundOutputView, + final Generation generation) { + return new RoundController(roundInputView, roundOutputView, generation); + } + + public void run() { + Round round = Round.nextRound(generation); + int strike; + + do { + RoundInputDto roundInputDto = roundInputView.roundUserInput(); + List userNumbers = roundInputDto.numbers(); + Balls userBalls = Balls.from(userNumbers); + RoundOutputDto roundOutputDto = round.countResult(userBalls); + strike = roundOutputDto.strike(); + roundOutputView.roundOutput(roundOutputDto); + } while (!round.hasEnough(strike)); + + roundOutputView.roundOverOutput(); + } + +} diff --git a/src/main/java/baseball/domain/Ball.java b/src/main/java/baseball/domain/Ball.java new file mode 100644 index 000000000..b33d65f31 --- /dev/null +++ b/src/main/java/baseball/domain/Ball.java @@ -0,0 +1,52 @@ +package baseball.domain; + +import baseball.domain.vo.BallNumber; +import baseball.domain.vo.Position; + +import java.util.Objects; + +public class Ball { + + private final BallNumber number; + private final Position position; + + private Ball(final BallNumber number, final Position position) { + this.number = number; + this.position = position; + } + + public static Ball of(final int number, final int position) { + BallNumber ballNumber = BallNumber.from(number); + Position ballPosition = Position.from(position); + return new Ball(ballNumber, ballPosition); + } + + public BallStatus compare(final Ball other) { + if (this.equals(other)) { + return BallStatus.STRIKE; + } + + if (other.hasSameNumber(number)) { + return BallStatus.BALL; + } + + return BallStatus.NOTHING; + } + + private boolean hasSameNumber(final BallNumber number) { + return this.number.equals(number); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof Ball)) return false; + final Ball ball = (Ball) o; + return number.equals(ball.number) && position.equals(ball.position); + } + + @Override + public int hashCode() { + return Objects.hash(number, position); + } +} diff --git a/src/main/java/baseball/domain/BallStatus.java b/src/main/java/baseball/domain/BallStatus.java new file mode 100644 index 000000000..251879f1a --- /dev/null +++ b/src/main/java/baseball/domain/BallStatus.java @@ -0,0 +1,15 @@ +package baseball.domain; + +public enum BallStatus { + STRIKE, + BALL, + NOTHING; + + public boolean isStrike() { + return this == STRIKE; + } + + public boolean isBall() { + return this == BALL; + } +} diff --git a/src/main/java/baseball/domain/BallStatuses.java b/src/main/java/baseball/domain/BallStatuses.java new file mode 100644 index 000000000..460207999 --- /dev/null +++ b/src/main/java/baseball/domain/BallStatuses.java @@ -0,0 +1,37 @@ +package baseball.domain; + +import java.util.List; + +public class BallStatuses { + + private static final int BALL_STATUSES_SIZE = 3; + + private final List statuses; + + private BallStatuses(final List statuses) { + this.statuses = statuses; + } + + public static BallStatuses from(final List statuses) { + validate(statuses); + return new BallStatuses(statuses); + } + + private static void validate(final List statuses) { + if (statuses.size() != BALL_STATUSES_SIZE) { + throw new IllegalArgumentException("숫자야구 상태의 길이는 3이어야 합니다"); + } + } + + public int countStrike() { + return (int) statuses.stream() + .filter(BallStatus::isStrike) + .count(); + } + + public int countBall() { + return (int) statuses.stream() + .filter(BallStatus::isBall) + .count(); + } +} diff --git a/src/main/java/baseball/domain/Balls.java b/src/main/java/baseball/domain/Balls.java new file mode 100644 index 000000000..25c03f6b3 --- /dev/null +++ b/src/main/java/baseball/domain/Balls.java @@ -0,0 +1,67 @@ +package baseball.domain; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public class Balls { + + private static final int START_POSITION = 1; + private static final int MAX_POSITION = 3; + + private final List values; + + private Balls(final List values) { + this.values = values; + } + + public static Balls from(final Collection numbers) { + validate(numbers); + List balls = changeBallsFrom(numbers); + return new Balls(balls); + } + + private static List changeBallsFrom(final Collection numbers) { + AtomicInteger position = new AtomicInteger(START_POSITION); + + return numbers.stream() + .map(number -> Ball.of(number, position.getAndIncrement())) + .collect(Collectors.toList()); + } + + private static void validate(Collection numbers) { + List distinctNumber = extractDistinctNumber(numbers); + + if (distinctNumber.size() != numbers.size()) { + throw new IllegalArgumentException("중복된 수가 존재합니다"); + } + + if (distinctNumber.size() != MAX_POSITION) { + throw new IllegalArgumentException("길이가 3이어야 합니다"); + } + } + + private static List extractDistinctNumber(final Collection numbers) { + Set set = new LinkedHashSet<>(numbers); + return new ArrayList<>(set); + } + + public BallStatuses compare(final Balls other) { + List statuses = values.stream() + .map(other::compareBallStatus) + .collect(Collectors.toList()); + return BallStatuses.from(statuses); + } + + public BallStatus compareBallStatus(final Ball other) { + return values.stream() + .map(ball -> ball.compare(other)) + .filter(ballStatus -> ballStatus.isStrike() || ballStatus.isBall()) + .findAny() + .orElse(BallStatus.NOTHING); + } +} diff --git a/src/main/java/baseball/domain/GameStatus.java b/src/main/java/baseball/domain/GameStatus.java new file mode 100644 index 000000000..630f8f85c --- /dev/null +++ b/src/main/java/baseball/domain/GameStatus.java @@ -0,0 +1,21 @@ +package baseball.domain; + +import java.util.Arrays; + +public enum GameStatus { + PLAY("1"), + OVER("2"); + + private final String status; + + GameStatus(final String status) { + this.status = status; + } + + public static GameStatus from(final String status) { + return Arrays.stream(GameStatus.values()) + .filter(value -> value.status.equals(status)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("잘못된 게임의 상태입니다")); + } +} diff --git a/src/main/java/baseball/domain/Generation.java b/src/main/java/baseball/domain/Generation.java new file mode 100644 index 000000000..e8f537856 --- /dev/null +++ b/src/main/java/baseball/domain/Generation.java @@ -0,0 +1,6 @@ +package baseball.domain; + +public interface Generation { + + int generate(); +} diff --git a/src/main/java/baseball/domain/RandomNumberGeneration.java b/src/main/java/baseball/domain/RandomNumberGeneration.java new file mode 100644 index 000000000..03bce148b --- /dev/null +++ b/src/main/java/baseball/domain/RandomNumberGeneration.java @@ -0,0 +1,25 @@ +package baseball.domain; + +import java.util.Random; + +public class RandomNumberGeneration implements Generation { + + private static final int START_INCLUSIVE = 1; + private static final int END_INCLUSIVE = 9; + + private final Random random; + + public RandomNumberGeneration(final Random random) { + this.random = random; + } + + + public static Generation from(final Random random) { + return new RandomNumberGeneration(random); + } + + @Override + public int generate() { + return START_INCLUSIVE + random.nextInt(START_INCLUSIVE + END_INCLUSIVE - START_INCLUSIVE); + } +} diff --git a/src/main/java/baseball/domain/Round.java b/src/main/java/baseball/domain/Round.java new file mode 100644 index 000000000..3be9f1c77 --- /dev/null +++ b/src/main/java/baseball/domain/Round.java @@ -0,0 +1,48 @@ +package baseball.domain; + +import baseball.dto.RoundOutputDto; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class Round { + + private static final int ENOUGH_STRIKE_INCLUSIVE = 3; + private static final int BASEBALL_LENGTH = 3; + + private final Balls balls; + + private Round(final Balls balls) { + this.balls = balls; + } + + public static Round from(Balls balls) { + return new Round(balls); + } + + public static Round nextRound(final Generation generation) { + Set numbers = generateNumbers(generation); + Balls balls = Balls.from(numbers); + return new Round(balls); + } + + private static Set generateNumbers(final Generation generation) { + Set set = new LinkedHashSet<>(); + while (set.size() < BASEBALL_LENGTH) { + int systemNumber = generation.generate(); + set.add(systemNumber); + } + return set; + } + + public RoundOutputDto countResult(Balls userBalls) { + BallStatuses ballStatuses = balls.compare(userBalls); + int strike = ballStatuses.countStrike(); + int ball = ballStatuses.countBall(); + return RoundOutputDto.of(strike, ball); + } + + public boolean hasEnough(final int strike) { + return strike == ENOUGH_STRIKE_INCLUSIVE; + } +} diff --git a/src/main/java/baseball/domain/vo/BallNumber.java b/src/main/java/baseball/domain/vo/BallNumber.java new file mode 100644 index 000000000..6fe9b81cc --- /dev/null +++ b/src/main/java/baseball/domain/vo/BallNumber.java @@ -0,0 +1,39 @@ +package baseball.domain.vo; + +import java.util.Objects; + +public class BallNumber { + + private static final int MIN_BALL_NUMBER = 1; + private static final int MAX_BALL_NUMBER = 9; + + private final int number; + + private BallNumber(final int number) { + this.number = number; + } + + public static BallNumber from(final int number) { + validate(number); + return new BallNumber(number); + } + + private static void validate(final int number) { + if (number < MIN_BALL_NUMBER || number > MAX_BALL_NUMBER) { + throw new IllegalArgumentException("1~9 사이의 숫자를 입력해야 합니다"); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof BallNumber)) return false; + final BallNumber that = (BallNumber) o; + return number == that.number; + } + + @Override + public int hashCode() { + return Objects.hash(number); + } +} diff --git a/src/main/java/baseball/domain/vo/Position.java b/src/main/java/baseball/domain/vo/Position.java new file mode 100644 index 000000000..a39e985ee --- /dev/null +++ b/src/main/java/baseball/domain/vo/Position.java @@ -0,0 +1,39 @@ +package baseball.domain.vo; + +import java.util.Objects; + +public class Position { + + private static final int MIN_POSITION = 1; + private static final int MAX_POSITION = 3; + + private final int value; + + private Position(final int value) { + this.value = value; + } + + public static Position from(final int value) { + validate(value); + return new Position(value); + } + + private static void validate(final int value) { + if (value < MIN_POSITION || value > MAX_POSITION) { + throw new IllegalArgumentException("1~3 사이의 위치를 입력해야 합니다"); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof Position)) return false; + final Position position = (Position) o; + return value == position.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/baseball/dto/RoundInputDto.java b/src/main/java/baseball/dto/RoundInputDto.java new file mode 100644 index 000000000..730d1be76 --- /dev/null +++ b/src/main/java/baseball/dto/RoundInputDto.java @@ -0,0 +1,47 @@ +package baseball.dto; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class RoundInputDto { + + private final List numbers; + + private RoundInputDto(List numbers) { + this.numbers = numbers; + } + + public static RoundInputDto from(final String[] userInputTokens) { + validate(userInputTokens); + List numbers = extractNumbersFrom(userInputTokens); + return new RoundInputDto(numbers); + } + + private static List extractNumbersFrom(final String[] userInputTokens) { + return Arrays.stream(userInputTokens) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + private static void validate(final String[] userInputTokens) { + if (hasNonDigitNumber(userInputTokens)) { + throw new IllegalArgumentException("숫자 이외의 문자가 포함되어 있습니다"); + } + } + + private static boolean hasNonDigitNumber(final String[] userInputTokens) { + return Arrays.stream(userInputTokens) + .anyMatch(RoundInputDto::containsNonDigitNumber); + } + + private static boolean containsNonDigitNumber(final String userInputToken) { + return userInputToken.equals("") || + userInputToken.chars() + .anyMatch(token -> !Character.isDigit(token)); + } + + public List numbers() { + return numbers; + } +} diff --git a/src/main/java/baseball/dto/RoundOutputDto.java b/src/main/java/baseball/dto/RoundOutputDto.java new file mode 100644 index 000000000..9a59df804 --- /dev/null +++ b/src/main/java/baseball/dto/RoundOutputDto.java @@ -0,0 +1,37 @@ +package baseball.dto; + +public class RoundOutputDto { + + private static final int ZERO = 0; + + private final int strike; + private final int ball; + private final boolean isThreeOut; + + public RoundOutputDto(final int strike, final int ball, final boolean isThreeOut) { + this.strike = strike; + this.ball = ball; + this.isThreeOut = isThreeOut; + } + + public static RoundOutputDto of(final int strike, final int ball) { + boolean isThreeOut = areStrikesAndBallsZero(strike, ball); + return new RoundOutputDto(strike, ball, isThreeOut); + } + + private static boolean areStrikesAndBallsZero(final int strike, final int ball) { + return strike == ZERO && ball == ZERO; + } + + public int ball() { + return ball; + } + + public int strike() { + return strike; + } + + public boolean isThreeOut() { + return isThreeOut; + } +} diff --git a/src/main/java/baseball/view/GameConsoleView.java b/src/main/java/baseball/view/GameConsoleView.java new file mode 100644 index 000000000..b5c05689b --- /dev/null +++ b/src/main/java/baseball/view/GameConsoleView.java @@ -0,0 +1,25 @@ +package baseball.view; + +import baseball.domain.GameStatus; + +import java.util.Scanner; + +public class GameConsoleView implements GameView { + + private final Scanner scanner; + + private GameConsoleView(final Scanner scanner) { + this.scanner = scanner; + } + + public static GameConsoleView from(final Scanner scanner) { + return new GameConsoleView(scanner); + } + + @Override + public GameStatus gameInput() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + String status = scanner.nextLine(); + return GameStatus.from(status); + } +} diff --git a/src/main/java/baseball/view/GameView.java b/src/main/java/baseball/view/GameView.java new file mode 100644 index 000000000..46531f268 --- /dev/null +++ b/src/main/java/baseball/view/GameView.java @@ -0,0 +1,8 @@ +package baseball.view; + +import baseball.domain.GameStatus; + +public interface GameView { + + GameStatus gameInput(); +} diff --git a/src/main/java/baseball/view/RoundInputConsoleView.java b/src/main/java/baseball/view/RoundInputConsoleView.java new file mode 100644 index 000000000..8576544c6 --- /dev/null +++ b/src/main/java/baseball/view/RoundInputConsoleView.java @@ -0,0 +1,28 @@ +package baseball.view; + +import baseball.dto.RoundInputDto; + +import java.util.Scanner; + +public class RoundInputConsoleView implements RoundInputView { + + private static final String DELIMITER = ""; + + private final Scanner scanner; + + private RoundInputConsoleView(final Scanner scanner) { + this.scanner = scanner; + } + + public static RoundInputView from(final Scanner scanner) { + return new RoundInputConsoleView(scanner); + } + + @Override + public RoundInputDto roundUserInput() { + System.out.print("숫자를 입력해 주세요 : "); + String userInput = scanner.nextLine(); + String[] userInputToken = userInput.split(DELIMITER); + return RoundInputDto.from(userInputToken); + } +} diff --git a/src/main/java/baseball/view/RoundInputView.java b/src/main/java/baseball/view/RoundInputView.java new file mode 100644 index 000000000..dd47b25bd --- /dev/null +++ b/src/main/java/baseball/view/RoundInputView.java @@ -0,0 +1,8 @@ +package baseball.view; + +import baseball.dto.RoundInputDto; + +public interface RoundInputView { + + RoundInputDto roundUserInput(); +} diff --git a/src/main/java/baseball/view/RoundOutputConsoleView.java b/src/main/java/baseball/view/RoundOutputConsoleView.java new file mode 100644 index 000000000..633e749ef --- /dev/null +++ b/src/main/java/baseball/view/RoundOutputConsoleView.java @@ -0,0 +1,60 @@ +package baseball.view; + +import baseball.dto.RoundOutputDto; + + +public class RoundOutputConsoleView implements RoundOutputView{ + + private static final int ZERO = 0; + private static final String EMPTY = ""; + + private RoundOutputConsoleView() { + } + + public static RoundOutputView getInstance() { + return new RoundOutputConsoleView(); + } + + @Override + public void roundOutput(RoundOutputDto roundOutputDto) { + String ball = parseBallOutput(roundOutputDto.ball()); + String strike = parseStrikeOutput(roundOutputDto.strike()); + String threeOut = parseThreeOutOutput(roundOutputDto.isThreeOut()); + String result = trimFrom(ball, strike, threeOut); + System.out.println(result); + } + + @Override + public void roundOverOutput() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료"); + } + + private String trimFrom(final String ball, final String strike, final String threeOut) { + return (ball + strike + threeOut).trim(); + } + + private String parseBallOutput(final int ball) { + if (ball != ZERO) { + return ball + "볼 "; + } + + return EMPTY; + } + + private String parseStrikeOutput(final int strike) { + if (strike != ZERO) { + return strike + "스트라이크 "; + } + + return EMPTY; + } + + private String parseThreeOutOutput(final boolean threeOut) { + if(threeOut) { + return "낫싱"; + } + + return EMPTY; + } + +} diff --git a/src/main/java/baseball/view/RoundOutputView.java b/src/main/java/baseball/view/RoundOutputView.java new file mode 100644 index 000000000..80f0fbc1f --- /dev/null +++ b/src/main/java/baseball/view/RoundOutputView.java @@ -0,0 +1,10 @@ +package baseball.view; + +import baseball.dto.RoundOutputDto; + +public interface RoundOutputView { + + void roundOutput(RoundOutputDto roundOutputDto); + + void roundOverOutput(); +} diff --git a/src/test/java/baseball/domain/BallStatusTest.java b/src/test/java/baseball/domain/BallStatusTest.java new file mode 100644 index 000000000..b3441b972 --- /dev/null +++ b/src/test/java/baseball/domain/BallStatusTest.java @@ -0,0 +1,25 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class BallStatusTest { + + @ParameterizedTest + @CsvSource(value = {"STRIKE,true", "BALL,false","NOTHING,false"}) + @DisplayName("숫자 야구의 상태가 스트라이크 여부를 확인한다") + void isStrike(BallStatus ballStatus, boolean expected) { + assertThat(ballStatus.isStrike()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"STRIKE,false", "BALL,true", "NOTHING,false"}) + @DisplayName("숫자 야구의 상태가 볼 여부를 확인한다") + void isBall(BallStatus ballStatus, boolean expected) { + assertThat(ballStatus.isBall()).isEqualTo(expected); + } + +} diff --git a/src/test/java/baseball/domain/BallStatusesTest.java b/src/test/java/baseball/domain/BallStatusesTest.java new file mode 100644 index 000000000..e45f505ca --- /dev/null +++ b/src/test/java/baseball/domain/BallStatusesTest.java @@ -0,0 +1,62 @@ +package baseball.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class BallStatusesTest { + + private BallStatuses ballStatuses; + + @BeforeEach + void setUp() { + List statuses = List.of(BallStatus.STRIKE, BallStatus.BALL, BallStatus.NOTHING); + ballStatuses = BallStatuses.from(statuses); + } + + @Test + @DisplayName("스트라이크의 개수를 반환한다") + void countStrike() { + int expected = 1; + + int actual = ballStatuses.countStrike(); + + assertThat(actual).isEqualTo(expected); + } + + + @Test + @DisplayName("스트라이크의 개수를 반환한다") + void countBall() { + int expected = 1; + + int actual = ballStatuses.countBall(); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("invalidBallStatusesArgument") + @DisplayName("숫자야구공들의 길이가 3이 아니면 예외를 발생시킨다") + void create_throw_exception_with_invalid_ball_statuses_size(List invalidStatuses) { + assertThatIllegalArgumentException().isThrownBy(() -> BallStatuses.from(invalidStatuses)) + .withMessage("숫자야구 상태의 길이는 3이어야 합니다"); + } + + static Stream invalidBallStatusesArgument() { + return Stream.of( + arguments(List.of(BallStatus.STRIKE, BallStatus.BALL)), + arguments(List.of(BallStatus.STRIKE, BallStatus.STRIKE, BallStatus.BALL, BallStatus.NOTHING)) + ); + } +} diff --git a/src/test/java/baseball/domain/BallTest.java b/src/test/java/baseball/domain/BallTest.java new file mode 100644 index 000000000..7b87c9306 --- /dev/null +++ b/src/test/java/baseball/domain/BallTest.java @@ -0,0 +1,32 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class BallTest { + + @ParameterizedTest + @MethodSource("compareTestArgument") + @DisplayName("숫자 야구를 비교해서 상태를 반환한다") + void compare(Ball ball, Ball other, BallStatus expected) { + + BallStatus actual = ball.compare(other); + + assertThat(actual).isEqualTo(expected); + } + + static Stream compareTestArgument() { + return Stream.of( + arguments(Ball.of(1, 1), Ball.of(2, 2), BallStatus.NOTHING), + arguments(Ball.of(1, 1), Ball.of(1, 2), BallStatus.BALL), + arguments(Ball.of(1, 1), Ball.of(1, 1), BallStatus.STRIKE) + ); + } +} diff --git a/src/test/java/baseball/domain/BallsTest.java b/src/test/java/baseball/domain/BallsTest.java new file mode 100644 index 000000000..dd3247422 --- /dev/null +++ b/src/test/java/baseball/domain/BallsTest.java @@ -0,0 +1,56 @@ +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.CsvSource; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class BallsTest { + + @Test + @DisplayName("숫자 야구 공의 길이가 3이 아니라면 예외를 발생시킨다") + void create_throws_exception_with_invalid_length() { + List numbers = Arrays.asList(1, 2, 3, 9); + + assertThatIllegalArgumentException().isThrownBy(() -> Balls.from(numbers)) + .withMessage("길이가 3이어야 합니다"); + } + + @Test + @DisplayName("숫자 야구공의 숫자에 중복이 존재하면 예외를 발생시킨다") + void create_throws_exception_with_duplicate_numbers() { + List numbers = Arrays.asList(1, 2, 2); + + assertThatIllegalArgumentException().isThrownBy(() -> Balls.from(numbers)) + .withMessage("중복된 수가 존재합니다"); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,STRIKE", "1,2,BALL", "4,1,NOTHING"}) + @DisplayName("숫자 야구 공들을 입력받아 볼의 상태를 반환한다") + void compareBallStatus(int number, int position, BallStatus expected) { + Balls balls = Balls.from(Arrays.asList(1, 2, 3)); + Ball other = Ball.of(number, position); + + BallStatus actual = balls.compareBallStatus(other); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("숫자 야구 공들을 입력받아 볼의 상태들을 반환한다") + void compare() { + Balls balls = Balls.from(List.of(1, 2, 3)); + Balls others = Balls.from(List.of(1, 3, 7)); + + BallStatuses actual = balls.compare(others); + + assertThat(actual).isNotNull(); + } +} diff --git a/src/test/java/baseball/domain/GameStatusTest.java b/src/test/java/baseball/domain/GameStatusTest.java new file mode 100644 index 000000000..8d8d1d40b --- /dev/null +++ b/src/test/java/baseball/domain/GameStatusTest.java @@ -0,0 +1,28 @@ +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.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class GameStatusTest { + + @ParameterizedTest + @CsvSource(value = {"PLAY,1", "OVER,2"}) + @DisplayName("게임의 상태를 반환한다") + void status(GameStatus gameStatus, String status) { + assertThat(gameStatus).isEqualTo(GameStatus.from(status)); + } + + @Test + @DisplayName("올바르지 않은 게임 상태는 예외를 발생시킨다") + void from_throw_exception_with_invalid_status() { + String invalidStatus = "3"; + + assertThatIllegalArgumentException().isThrownBy(() -> GameStatus.from(invalidStatus)) + .withMessage("잘못된 게임의 상태입니다"); + } +} diff --git a/src/test/java/baseball/domain/GenerateRandomNumberTest.java b/src/test/java/baseball/domain/GenerateRandomNumberTest.java new file mode 100644 index 000000000..57fa7a17b --- /dev/null +++ b/src/test/java/baseball/domain/GenerateRandomNumberTest.java @@ -0,0 +1,21 @@ +package baseball.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +class GenerateRandomNumberTest { + + @Test + @DisplayName("1~9 사이의 난수를 생성한다") + void generate() { + Generation generation = RandomNumberGeneration.from(new Random()); + + int actual = generation.generate(); + + assertThat(actual).isStrictlyBetween(0, 10); + } +} diff --git a/src/test/java/baseball/domain/RoundTest.java b/src/test/java/baseball/domain/RoundTest.java new file mode 100644 index 000000000..b292cc174 --- /dev/null +++ b/src/test/java/baseball/domain/RoundTest.java @@ -0,0 +1,58 @@ +package baseball.domain; + +import baseball.dto.RoundOutputDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class RoundTest { + + @Test + @DisplayName("스트라이크, 볼, 쓰리아웃 여부를 반환한다") + void countResult() { + List numbers = Arrays.asList(1, 2, 3); + Balls balls = Balls.from(numbers); + Round round = Round.from(balls); + List userNumbers = Arrays.asList(2, 1, 3); + Balls userBalls = Balls.from(userNumbers); + + RoundOutputDto roundOutputDto = round.countResult(userBalls); + + assertAll( + () -> assertThat(roundOutputDto.strike()).isEqualTo(1), + () -> assertThat(roundOutputDto.ball()).isEqualTo(2) + ); + } + + @ParameterizedTest + @MethodSource("ballParameterProvider") + @DisplayName("라운드의 종료 여부를 반환한다") + void isOver(List numbers, boolean expect) { + Balls balls = Balls.from(numbers); + Round round = Round.from(balls); + List userNumbers = Arrays.asList(1, 2, 3); + Balls userBalls = Balls.from(userNumbers); + RoundOutputDto roundOutputDto = round.countResult(userBalls); + + boolean actual = round.hasEnough(roundOutputDto.strike()); + + assertThat(actual).isEqualTo(expect); + } + + static Stream ballParameterProvider() { + return Stream.of( + arguments(Arrays.asList(1, 2, 3), true), + arguments(Arrays.asList(7, 8, 9), false) + ); + } +} diff --git a/src/test/java/baseball/domain/vo/BallNumberTest.java b/src/test/java/baseball/domain/vo/BallNumberTest.java new file mode 100644 index 000000000..12bd0da25 --- /dev/null +++ b/src/test/java/baseball/domain/vo/BallNumberTest.java @@ -0,0 +1,32 @@ +package baseball.domain.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class BallNumberTest { + + @ParameterizedTest + @ValueSource(ints = {0, 10}) + @DisplayName("1~9 이외의 수를 입력받으면 예외를 발생시킨다") + void create_throw_exception_with_invalid_number(int number) { + assertThatIllegalArgumentException().isThrownBy(() -> BallNumber.from(number)) + .withMessage("1~9 사이의 숫자를 입력해야 합니다"); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,2,false"}) + @DisplayName("공의 숫자가 같으면 참 다르면 거짓을 반환한다.") + void equals(int number, int other, boolean expected) { + BallNumber ballNumber = BallNumber.from(number); + BallNumber otherNumber = BallNumber.from(other); + + boolean actual = ballNumber.equals(otherNumber); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/baseball/domain/vo/PositionTest.java b/src/test/java/baseball/domain/vo/PositionTest.java new file mode 100644 index 000000000..75553d026 --- /dev/null +++ b/src/test/java/baseball/domain/vo/PositionTest.java @@ -0,0 +1,33 @@ +package baseball.domain.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class PositionTest { + + + @ParameterizedTest + @ValueSource(ints = {0, 4}) + @DisplayName("1~3 사이의 위치값이 아니면 예외를 발생시킨다") + void create_throw_exception_with_invalid_position(int value) { + assertThatIllegalArgumentException().isThrownBy(() -> Position.from(value)) + .withMessage("1~3 사이의 위치를 입력해야 합니다"); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,2,false"}) + @DisplayName("위치가 같으면 참, 다르면 거짓을 반환한다") + void equals(int value, int otherPosition, boolean expected) { + Position position = Position.from(value); + Position other = Position.from(otherPosition); + + boolean actual = position.equals(other); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/baseball/dto/RoundInputDtoTest.java b/src/test/java/baseball/dto/RoundInputDtoTest.java new file mode 100644 index 000000000..a038a7f4a --- /dev/null +++ b/src/test/java/baseball/dto/RoundInputDtoTest.java @@ -0,0 +1,28 @@ +package baseball.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class RoundInputDtoTest { + + @Test + @DisplayName("사용자의 입력을 받아 값을 반환한다") + void numbers() { + String[] userInputToken = new String[]{"1", "2", "3"}; + + RoundInputDto userInputDto = RoundInputDto.from(userInputToken); + + assertThat(userInputDto.numbers()).containsExactly(1, 2, 3); + } + @Test + @DisplayName("사용자의 입력 중에서 숫자가 아닌 수가 포함되어 있을 경우 예외를 발생시킨다") + void create_throws_exception_include_not_digit_token() { + String[] userInputToken = new String[]{"한", "/", " "}; + + assertThatIllegalArgumentException().isThrownBy(() -> RoundInputDto.from(userInputToken)) + .withMessage("숫자 이외의 문자가 포함되어 있습니다"); + } +} diff --git a/src/test/java/baseball/dto/RoundOutputDtoTest.java b/src/test/java/baseball/dto/RoundOutputDtoTest.java new file mode 100644 index 000000000..173ed7936 --- /dev/null +++ b/src/test/java/baseball/dto/RoundOutputDtoTest.java @@ -0,0 +1,24 @@ +package baseball.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RoundOutputDtoTest { + + @ParameterizedTest + @CsvSource(value = {"1,1,false", "0,0,true"}) + @DisplayName("스트라이크, 볼의 개수를 입력 받아 스트라이크, 볼, 낫싱 여부를 반환한다") + void create(final int strike, final int ball, boolean expectedThreeOut) { + RoundOutputDto roundOutputDto = RoundOutputDto.of(strike, ball); + + assertAll( + () -> assertThat(roundOutputDto.strike()).isEqualTo(strike), + () -> assertThat(roundOutputDto.ball()).isEqualTo(ball), + () -> assertThat(roundOutputDto.isThreeOut()).isEqualTo(expectedThreeOut) + ); + } +}