diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34214..f20d5fa028 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,39 @@ package baseball; +import baseball.domain.GameResult; +import baseball.service.GameService; +import camp.nextstep.edu.missionutils.Console; + public class Application { + public static void main(String[] args) { - // TODO: 프로그램 구현 + while (true) { + playGame(); + + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + + final RestartMessage restartMessage = new RestartMessage(Console.readLine()); + if (restartMessage.isEnd()) { + System.out.println("게임 종료"); + break; + } + } + } + + private static void playGame() { + GameService gameService = new GameService(); + + System.out.println("숫자 야구 게임을 시작합니다."); + while (true) { + System.out.print("숫자를 입력해주세요 : "); + String playerInput = Console.readLine(); + + GameResult result = gameService.findResult(playerInput); + + System.out.println(result.getResultComment()); + if (result.isEnd()) { + break; + } + } } } diff --git a/src/main/java/baseball/RestartMessage.java b/src/main/java/baseball/RestartMessage.java new file mode 100644 index 0000000000..467b2135cd --- /dev/null +++ b/src/main/java/baseball/RestartMessage.java @@ -0,0 +1,28 @@ +package baseball; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RestartMessage { + + private static final String RESTART_MESSAGE = "1"; + private static final String END_MESSAGE = "2"; + private static final Pattern PATTERN = Pattern.compile(String.format("[%s%s]", RESTART_MESSAGE, END_MESSAGE)); + private final String value; + + public RestartMessage(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + final Matcher matcher = PATTERN.matcher(value); + if (!matcher.matches()) { + throw new IllegalArgumentException(String.format("[ERROR] 재시작 입력 값은 %s 일 수 없습니다.", value)); + } + } + + public boolean isEnd() { + return this.value.equals(END_MESSAGE); + } +} diff --git a/src/main/java/baseball/domain/GameNumber.java b/src/main/java/baseball/domain/GameNumber.java new file mode 100644 index 0000000000..0eca55de80 --- /dev/null +++ b/src/main/java/baseball/domain/GameNumber.java @@ -0,0 +1,45 @@ +package baseball.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.Objects; + +public class GameNumber { + + private static final int MIN_VALUE = 1; + private static final int MAX_VALUE = 9; + + private final int value; + + public GameNumber(int value) { + validateRange(value); + this.value = value; + } + + private void validateRange(int value) { + if (value < MIN_VALUE || value > MAX_VALUE) { + throw new IllegalArgumentException(String.format("[ERROR] 야구게임 숫자는 %d일 수 없습니다", value)); + } + } + + public static GameNumber createRandomNumber() { + final int number = Randoms.pickNumberInRange(MIN_VALUE, MAX_VALUE); + return new GameNumber(number); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GameNumber that = (GameNumber) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/baseball/domain/GameNumbers.java b/src/main/java/baseball/domain/GameNumbers.java new file mode 100644 index 0000000000..de59817bc8 --- /dev/null +++ b/src/main/java/baseball/domain/GameNumbers.java @@ -0,0 +1,65 @@ +package baseball.domain; + +import baseball.domain.gamenumbercreator.GameNumberCreator; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +public class GameNumbers { + + private static final int MAX_LENGTH = 3; + private final List numbers; + + private GameNumbers(List numbers) { + validateLength(numbers); + validateDuplicate(numbers); + this.numbers = new ArrayList<>(numbers); + } + + public static GameNumbers from(GameNumberCreator numberCreator) { + return new GameNumbers(numberCreator.create(MAX_LENGTH)); + } + + private void validateLength(List numbers) { + if (numbers.size() != MAX_LENGTH) { + throw new IllegalArgumentException(String.format("[ERROR] 게임 숫자는 %d 자리여야합니다.", MAX_LENGTH)); + } + } + + private void validateDuplicate(List numbers) { + if (numbers.size() != new HashSet<>(numbers).size()) { + throw new IllegalArgumentException("[ERROR] 게임 숫자는 중복된 값을 가질 수 없습니다."); + } + } + + public GameResult calculateResult(GameNumbers other) { + int strike = (int) IntStream.range(0, other.numbers.size()) + .filter(i -> other.numbers.get(i).equals(this.numbers.get(i))) + .count(); + + int ball = (int) other.numbers.stream() + .filter(this.numbers::contains) + .count() - strike; + + return GameResult.find(strike, ball); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GameNumbers that = (GameNumbers) o; + return Objects.equals(numbers, that.numbers); + } + + @Override + public int hashCode() { + return Objects.hash(numbers); + } +} diff --git a/src/main/java/baseball/domain/GameResult.java b/src/main/java/baseball/domain/GameResult.java new file mode 100644 index 0000000000..f716011de3 --- /dev/null +++ b/src/main/java/baseball/domain/GameResult.java @@ -0,0 +1,55 @@ +package baseball.domain; + +import java.util.Arrays; + +public enum GameResult { + ZERO(0, 0), + ZERO_STRIKE_ONE_BALL(0, 1), + ZERO_STRIKE_TWO_BALL(0, 2), + ZERO_STRIKE_THREE_BALL(0, 3), + ONE_STRIKE_ZERO_BALL(1, 0), + ONE_STRIKE_ONE_BALL(1, 1), + ONE_STRIKE_TWO_BALL(1, 2), + TWO_STRIKE_ZERO_BALL(2, 0), + TWO_STRIKE_ONE_BALL(2, 1), + THREE_STRIKE(3, 0); + + private final int strike; + private final int ball; + + GameResult(int strike, int ball) { + this.strike = strike; + this.ball = ball; + } + + public static GameResult find(int strike, int ball) { + return Arrays.stream(GameResult.values()) + .filter(gameResult -> gameResult.strike == strike && gameResult.ball == ball) + .findAny() + .orElseThrow(() -> new IllegalArgumentException( + String.format("[ERROR] %d 스트라이크 %d 볼에 해달하는 결과는 존재하지 않습니다.", strike, ball))); + } + + public boolean isEnd() { + return this == THREE_STRIKE; + } + + public String getResultComment() { + if (strike == 0 && ball == 0) { + return "낫싱"; + } + + StringBuilder answer = new StringBuilder(); + if (ball != 0) { + answer.append(ball); + answer.append("볼 "); + } + + if (strike != 0) { + answer.append(strike); + answer.append("스트라이크"); + } + + return answer.toString(); + } +} diff --git a/src/main/java/baseball/domain/gamenumbercreator/GameNumberCreator.java b/src/main/java/baseball/domain/gamenumbercreator/GameNumberCreator.java new file mode 100644 index 0000000000..6364405361 --- /dev/null +++ b/src/main/java/baseball/domain/gamenumbercreator/GameNumberCreator.java @@ -0,0 +1,8 @@ +package baseball.domain.gamenumbercreator; + +import baseball.domain.GameNumber; +import java.util.List; + +public interface GameNumberCreator { + List create(int maxLength); +} diff --git a/src/main/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreator.java b/src/main/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreator.java new file mode 100644 index 0000000000..8c771ca8ea --- /dev/null +++ b/src/main/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreator.java @@ -0,0 +1,17 @@ +package baseball.domain.gamenumbercreator; + +import baseball.domain.GameNumber; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RandomIntegerToGameNumberCreator implements GameNumberCreator { + + @Override + public List create(int maxLength) { + return Stream.generate(GameNumber::createRandomNumber) + .distinct() + .limit(maxLength) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/baseball/domain/gamenumbercreator/StringToGameNumberCreator.java b/src/main/java/baseball/domain/gamenumbercreator/StringToGameNumberCreator.java new file mode 100644 index 0000000000..f50b22e69f --- /dev/null +++ b/src/main/java/baseball/domain/gamenumbercreator/StringToGameNumberCreator.java @@ -0,0 +1,39 @@ +package baseball.domain.gamenumbercreator; + +import baseball.domain.GameNumber; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class StringToGameNumberCreator implements GameNumberCreator { + + private final String values; + + public StringToGameNumberCreator(String values) { + this.values = values; + } + + @Override + public List create(int maxLength) { + validateLength(maxLength); + + return Arrays.stream(values.split("")) + .map(this::parseInt) + .map(GameNumber::new) + .collect(Collectors.toList()); + } + + private void validateLength(int maxLength) { + if (values.length() != maxLength) { + throw new IllegalArgumentException(String.format("[ERROR] %s는 %d 자리수가 아닙니다.", values, maxLength)); + } + } + + private Integer parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format("[ERROR] 입력값 %s는 숫자가 아닙니다.", value)); + } + } +} diff --git a/src/main/java/baseball/service/GameService.java b/src/main/java/baseball/service/GameService.java new file mode 100644 index 0000000000..c3379a31af --- /dev/null +++ b/src/main/java/baseball/service/GameService.java @@ -0,0 +1,23 @@ +package baseball.service; + +import baseball.domain.GameNumbers; +import baseball.domain.GameResult; +import baseball.domain.gamenumbercreator.GameNumberCreator; +import baseball.domain.gamenumbercreator.RandomIntegerToGameNumberCreator; +import baseball.domain.gamenumbercreator.StringToGameNumberCreator; + +public class GameService { + + private static final GameNumberCreator NUMBER_CREATOR = new RandomIntegerToGameNumberCreator(); + private final GameNumbers answerNumbers; + + public GameService() { + this.answerNumbers = GameNumbers.from(NUMBER_CREATOR); + } + + public GameResult findResult(String playerRequest) { + final StringToGameNumberCreator creator = new StringToGameNumberCreator(playerRequest); + final GameNumbers playerNumbers = GameNumbers.from(creator); + return answerNumbers.calculateResult(playerNumbers); + } +} diff --git a/src/test/java/baseball/ApplicationTest.java b/src/test/java/baseball/ApplicationTest.java index 3fa29fa67b..fcd4cde406 100644 --- a/src/test/java/baseball/ApplicationTest.java +++ b/src/test/java/baseball/ApplicationTest.java @@ -1,13 +1,13 @@ package baseball; -import camp.nextstep.edu.missionutils.test.NsTest; -import org.junit.jupiter.api.Test; - import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + class ApplicationTest extends NsTest { @Test void 게임종료_후_재시작() { diff --git a/src/test/java/baseball/domain/GameNumberTest.java b/src/test/java/baseball/domain/GameNumberTest.java new file mode 100644 index 0000000000..1f2b02b7e5 --- /dev/null +++ b/src/test/java/baseball/domain/GameNumberTest.java @@ -0,0 +1,23 @@ +package baseball.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class GameNumberTest { + + @ParameterizedTest + @ValueSource(ints = {0, 10}) + void 게임_숫자가_1이상_9이하의_자연수가_아니면_예외를_던진다(int number) { + Assertions.assertThatThrownBy(() -> new GameNumber(number)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9}) + void 게임_숫자는_1이상_9이하의_자연수여야한다(int number) { + Assertions.assertThatCode(() -> new GameNumber(number)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/baseball/domain/GameNumbersTest.java b/src/test/java/baseball/domain/GameNumbersTest.java new file mode 100644 index 0000000000..93f24fe490 --- /dev/null +++ b/src/test/java/baseball/domain/GameNumbersTest.java @@ -0,0 +1,75 @@ +package baseball.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import baseball.domain.gamenumbercreator.GameNumberCreator; +import java.util.List; +import java.util.stream.Stream; +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; + +class GameNumbersTest { + + public static Stream invalidGameNumbers() { + return Stream.of( + Arguments.of(List.of(new GameNumber(1), new GameNumber(2), new GameNumber(3), new GameNumber(4))), + Arguments.of(List.of(new GameNumber(1), new GameNumber(2))) + ); + } + + public static Stream numbers() { + final List computerNumbers = List.of(new GameNumber(1), new GameNumber(2), new GameNumber(3)); + + return Stream.of( + Arguments.of( + computerNumbers, + List.of(new GameNumber(1), new GameNumber(2), new GameNumber(3)), + GameResult.THREE_STRIKE), + Arguments.of( + computerNumbers, + List.of(new GameNumber(2), new GameNumber(1), new GameNumber(3)), + GameResult.ONE_STRIKE_TWO_BALL), + Arguments.of( + computerNumbers, + List.of(new GameNumber(7), new GameNumber(8), new GameNumber(9)), + GameResult.ZERO) + ); + + } + + @ParameterizedTest + @MethodSource("invalidGameNumbers") + void 게임_넘버가_3자리가_아니면_예외를_던진다(List gameNumbers) { + // given + GameNumberCreator creator = maxLength -> gameNumbers; + + assertThatThrownBy(() -> GameNumbers.from(creator)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 중복된_게임_넘버가_존재하면_예외를_던진다() { + // given + final List gameNumbers = List.of(new GameNumber(2), new GameNumber(2), new GameNumber(3)); + GameNumberCreator creator = maxLength -> gameNumbers; + + // when & then + assertThatThrownBy(() -> GameNumbers.from(creator)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @MethodSource("numbers") + void 게임_넘버들로_게임_결과를_생성할_수_있다(List com, List player, GameResult result) { + // given + final GameNumbers computerNumber = GameNumbers.from(maxLength -> com); + final GameNumbers playerNumber = GameNumbers.from(maxLength -> player); + + // when & then + assertThat(computerNumber.calculateResult(playerNumber)) + .isEqualTo(result); + } +} diff --git a/src/test/java/baseball/domain/GameResultTest.java b/src/test/java/baseball/domain/GameResultTest.java new file mode 100644 index 0000000000..a1ad2b28c8 --- /dev/null +++ b/src/test/java/baseball/domain/GameResultTest.java @@ -0,0 +1,23 @@ +package baseball.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class GameResultTest { + + @ParameterizedTest + @CsvSource(value = {"0, -1", "-1, 1"}) + void 올바르지않은_strike나_ball값으로_결과를_찾으면_예외를_던진다(int strike, int ball) { + Assertions.assertThatThrownBy(() -> GameResult.find(strike, ball)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"0, 0", "0, 1", "0, 2", "0, 3", "1, 0", "1, 1", "1, 2", "2, 0", "2, 1", "3, 0"}) + void 올바른_strike_ball값으로_결과를_찾을_수_있다(int strike, int ball) { + Assertions.assertThatCode(() -> GameResult.find(strike, ball)) + .doesNotThrowAnyException(); + } + +} diff --git a/src/test/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreatorTest.java b/src/test/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreatorTest.java new file mode 100644 index 0000000000..f9a99b9369 --- /dev/null +++ b/src/test/java/baseball/domain/gamenumbercreator/RandomIntegerToGameNumberCreatorTest.java @@ -0,0 +1,17 @@ +package baseball.domain.gamenumbercreator; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class RandomIntegerToGameNumberCreatorTest { + + @Test + void 숫자들을_통해_게임_숫자를_만들_수_있다() { + // given + final RandomIntegerToGameNumberCreator creator = new RandomIntegerToGameNumberCreator(); + + // when & then + Assertions.assertThatCode(() -> creator.create(3)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/baseball/domain/gamenumbercreator/StringToGameNumberCreatorTest.java b/src/test/java/baseball/domain/gamenumbercreator/StringToGameNumberCreatorTest.java new file mode 100644 index 0000000000..b2855af55e --- /dev/null +++ b/src/test/java/baseball/domain/gamenumbercreator/StringToGameNumberCreatorTest.java @@ -0,0 +1,30 @@ +package baseball.domain.gamenumbercreator; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StringToGameNumberCreatorTest { + + @ParameterizedTest + @ValueSource(strings = {"12 ", "-12", "ㅁ12", "1a3", "87?"}) + void 입력값에_숫자가_아닌_값이_존재하면_GameNumber생성_시_예외를_던진다(String values) { + // given + final StringToGameNumberCreator creator = new StringToGameNumberCreator(values); + + // when & then + Assertions.assertThatThrownBy(() -> creator.create(3)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 정상적인_수로_게임_넘버를_만들_수_있다() { + // given + final StringToGameNumberCreator creator = new StringToGameNumberCreator("123"); + + // when & then + Assertions.assertThatCode(() -> creator.create(3)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java index 462ab1c4a4..b8d0ba50a8 100644 --- a/src/test/java/study/StringTest.java +++ b/src/test/java/study/StringTest.java @@ -1,8 +1,9 @@ package study; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; public class StringTest {