diff --git a/docs/README.md b/docs/README.md index e69de29bb2..c9da2e53f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,13 @@ +## 기능 목록 + +### 데이터 처리 +- [x] 컴퓨터 수 생성 + - [x] 1 ~ 9 범위의 랜덤 수 한 개 생성 + - [x] 중복되지 않는 수 3개 생성 + - [x] 생성된 수로 컴퓨터 수 생성 +- [x] 비교 결과 생성 + - [x] 볼, 스트라이크 개수 구하기 + - [x] 공 한 개가 볼인지 스트라이크인지 판정하기 + +### 고민한 부분 +- [x] \ No newline at end of file diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34214..71e0b86ff3 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,21 @@ package baseball; +import baseball.controller.BaseballGameController; +import baseball.model.BaseballGame; +import baseball.model.ComputerBallsGenerator; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + BaseballGameController baseballGameController = new BaseballGameController(baseballGame()); + baseballGameController.run(); + } + + private static BaseballGame baseballGame() { + return new BaseballGame(computerBallsGenerator()); + } + + private static ComputerBallsGenerator computerBallsGenerator() { + return new ComputerBallsGenerator(); } } diff --git a/src/main/java/baseball/controller/BaseballGameController.java b/src/main/java/baseball/controller/BaseballGameController.java new file mode 100644 index 0000000000..b126a08553 --- /dev/null +++ b/src/main/java/baseball/controller/BaseballGameController.java @@ -0,0 +1,40 @@ +package baseball.controller; + +import baseball.model.BaseballGame; +import baseball.model.GameResult; +import baseball.view.InputView; +import baseball.view.OutputView; + +import java.util.List; + +public class BaseballGameController { + private final InputView inputView = InputView.getInstance(); + private final OutputView outputView = OutputView.getInstance(); + private final BaseballGame baseballGame; + + public BaseballGameController(BaseballGame baseballGame) { + this.baseballGame = baseballGame; + } + + public void run() { + initializeGame(); + while (baseballGame.isContinuing()) { + playAGame(); + baseballGame.checkRegame(inputView.inputGameCommand()); + } + } + + private void initializeGame() { + outputView.printStartingMessage(); + baseballGame.init(); + } + + private void playAGame() { + while (baseballGame.isContinuing()) { + List inputNumbers = inputView.inputNumbers(); + GameResult gameResult = baseballGame.compare(inputNumbers); + outputView.printResult(gameResult); + } + outputView.printWinningMessage(); + } +} diff --git a/src/main/java/baseball/model/Ball.java b/src/main/java/baseball/model/Ball.java new file mode 100644 index 0000000000..d720b9dcfb --- /dev/null +++ b/src/main/java/baseball/model/Ball.java @@ -0,0 +1,27 @@ +package baseball.model; + +public class Ball { + private final BallNumber number; + private final Position position; + + public Ball(BallNumber number, Position position) { + this.number = number; + this.position = position; + } + + public static Ball from(int number, int position) { + return new Ball(BallNumber.valueOf(number), Position.valueOf(position)); + } + + public boolean isBall(Ball other) { + return !position.equals(other.position) && number.equals(other.number); + } + + public boolean isStrike(Ball other) { + return position.equals(other.position) && number.equals(other.number); + } + + public BallNumber getNumber() { + return number; + } +} diff --git a/src/main/java/baseball/model/BallNumber.java b/src/main/java/baseball/model/BallNumber.java new file mode 100644 index 0000000000..0a539779c6 --- /dev/null +++ b/src/main/java/baseball/model/BallNumber.java @@ -0,0 +1,41 @@ +package baseball.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BallNumber { + public static final int LOWER_BOUNDS = 1; + public static final int UPPER_BOUNDS = 9; + private static final Map CACHE = new HashMap<>(); + private final int number; + + static { + for (int number = LOWER_BOUNDS; number <= UPPER_BOUNDS; number++) { + CACHE.put(number, new BallNumber(number)); + } + } + + private BallNumber(int number) { + validate(number); + this.number = number; + } + + private void validate(int ballNumber) { + if (ballNumber < LOWER_BOUNDS || ballNumber > UPPER_BOUNDS) { + throw new IllegalArgumentException(); + } + } + + public static BallNumber valueOf(int number) { + BallNumber ballNumber = CACHE.get(number); + if (Objects.isNull(ballNumber)) { + ballNumber = new BallNumber(number); + } + return ballNumber; + } + + public int getNumber() { + return number; + } +} diff --git a/src/main/java/baseball/model/Balls.java b/src/main/java/baseball/model/Balls.java new file mode 100644 index 0000000000..3f5c183964 --- /dev/null +++ b/src/main/java/baseball/model/Balls.java @@ -0,0 +1,68 @@ +package baseball.model; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Balls { + public static final int SIZE = 3; + + private final List balls; + + public static Balls from(List numbers) { + List ballList = IntStream.range(0, numbers.size()) + .mapToObj(i -> Ball.from(numbers.get(i), i)) + .collect(Collectors.toList()); + return new Balls(ballList); + } + + public Balls(List balls) { + validate(balls); + this.balls = balls; + } + + private void validate(List balls) { + validateSize(balls); + validateDuplication(balls); + } + + private void validateSize(List balls) { + if (balls.size() != SIZE) { + throw new IllegalArgumentException(); + } + } + + private void validateDuplication(List balls) { + long uniqueBallsCount = balls.stream() + .map(Ball::getNumber) + .distinct() + .count(); + + if (uniqueBallsCount != SIZE) { + throw new IllegalArgumentException(); + } + } + + public GameResult compare(Balls other) { + int ballCount = countFiltered(other, this::ballPredicate); + int strikeCount = countFiltered(other, this::strikePredicate); + return GameResult.of(ballCount, strikeCount); + } + + private int countFiltered(Balls other, Predicate predicate) { + return (int) other.balls.stream() + .filter(predicate) + .count(); + } + + private boolean ballPredicate(Ball otherBall) { + return balls.stream() + .anyMatch(ball -> ball.isBall(otherBall)); + } + + private boolean strikePredicate(Ball otherBall) { + return balls.stream() + .anyMatch(ball -> ball.isStrike(otherBall)); + } +} diff --git a/src/main/java/baseball/model/BaseballGame.java b/src/main/java/baseball/model/BaseballGame.java new file mode 100644 index 0000000000..bb629e9bc3 --- /dev/null +++ b/src/main/java/baseball/model/BaseballGame.java @@ -0,0 +1,42 @@ +package baseball.model; + +import java.util.List; + +public class BaseballGame { + private final ComputerBallsGenerator computerBallsGenerator; + private GameStatus gameStatus; + private Balls computerBalls; + + public BaseballGame(ComputerBallsGenerator computerBallsGenerator) { + this.computerBallsGenerator = computerBallsGenerator; + } + + public void init() { + gameStatus = GameStatus.CONTINUING; + computerBalls = computerBallsGenerator.generate(); + } + + public boolean isContinuing() { + return gameStatus.isContinuing(); + } + + public GameResult compare(List inputNumbers) { + Balls userBalls = Balls.from(inputNumbers); + GameResult result = computerBalls.compare(userBalls); + checkUserWin(result); + return result; + } + + private void checkUserWin(GameResult result) { + if (result.isUserWin()) { + gameStatus = GameStatus.STOP; + } + } + + public void checkRegame(GameCommand gameCommand) { + if (gameCommand.isRestart()) { + gameStatus = GameStatus.CONTINUING; + computerBalls = computerBallsGenerator.generate(); + } + } +} diff --git a/src/main/java/baseball/model/ComputerBallsGenerator.java b/src/main/java/baseball/model/ComputerBallsGenerator.java new file mode 100644 index 0000000000..e0f4a113f7 --- /dev/null +++ b/src/main/java/baseball/model/ComputerBallsGenerator.java @@ -0,0 +1,34 @@ +package baseball.model; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +public class ComputerBallsGenerator { + public Balls generate() { + List uniqueNumbers = generateUniqueNumbers(); + return IntStream.rangeClosed(Position.LOWER_BOUNDS, Position.UPPER_BOUNDS) + .mapToObj(position -> Ball.from(uniqueNumbers.get(position), position)) + .collect(collectingAndThen(toList(), Balls::new)); + } + + private List generateUniqueNumbers() { + Set uniqueNumbers = new HashSet<>(); + while (hasEnough(uniqueNumbers)) { + int number = Randoms.pickNumberInRange(BallNumber.LOWER_BOUNDS, BallNumber.UPPER_BOUNDS); + uniqueNumbers.add(number); + } + return new ArrayList<>(uniqueNumbers); + } + + private static boolean hasEnough(Set uniqueNumbers) { + return uniqueNumbers.size() < Balls.SIZE; + } +} diff --git a/src/main/java/baseball/model/GameCommand.java b/src/main/java/baseball/model/GameCommand.java new file mode 100644 index 0000000000..0d4a7643f0 --- /dev/null +++ b/src/main/java/baseball/model/GameCommand.java @@ -0,0 +1,25 @@ +package baseball.model; + +import java.util.Arrays; + +public enum GameCommand { + RESTART(1), + TERMINATION(2); + + private final int code; + + GameCommand(int code) { + this.code = code; + } + + public static GameCommand from(int code) { + return Arrays.stream(values()) + .filter(gameCommand -> gameCommand.code == code) + .findAny() + .orElseThrow(() -> new IllegalArgumentException()); + } + + public boolean isRestart() { + return this.equals(RESTART); + } +} diff --git a/src/main/java/baseball/model/GameResult.java b/src/main/java/baseball/model/GameResult.java new file mode 100644 index 0000000000..f61beda95c --- /dev/null +++ b/src/main/java/baseball/model/GameResult.java @@ -0,0 +1,44 @@ +package baseball.model; + +import java.util.Arrays; + +public enum GameResult { + STRIKE_3_BALL_0(3, 0, "3스트라이크"), + STRIKE_1_BALL_2(1, 2, "2볼 1스트라이크"), + STRIKE_0_BALL_3(0, 3, "3볼"), + STRIKE_2_BALL_0(2, 0, "2스트라이크"), + STRIKE_1_BALL_1(1, 1, "1볼 1스트라이크"), + STRIKE_0_BALL_2(0, 2, "2볼"), + STRIKE_1_BALL_0(1, 0, "1스트라이크"), + STRIKE_0_BALL_1(0, 1, "1볼"), + NOTHING(0, 0, "낫싱"); + + private final int strikeCount; + private final int ballCount; + private final String output; + + GameResult(int strikeCount, int ballCount, String output) { + this.strikeCount = strikeCount; + this.ballCount = ballCount; + this.output = output; + } + + public static GameResult of(int ballCount, int strikeCount) { + return Arrays.stream(values()) + .filter(gameResult -> gameResult.matches(ballCount, strikeCount)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException()); + } + + private boolean matches(int ballCount, int strikeCount) { + return this.strikeCount == strikeCount && this.ballCount == ballCount; + } + + public boolean isUserWin() { + return this.equals(STRIKE_3_BALL_0); + } + + public String getOutput() { + return output; + } +} diff --git a/src/main/java/baseball/model/GameStatus.java b/src/main/java/baseball/model/GameStatus.java new file mode 100644 index 0000000000..5266ba50b1 --- /dev/null +++ b/src/main/java/baseball/model/GameStatus.java @@ -0,0 +1,10 @@ +package baseball.model; + +public enum GameStatus { + CONTINUING, + STOP; + + public boolean isContinuing() { + return this.equals(CONTINUING); + } +} diff --git a/src/main/java/baseball/model/Position.java b/src/main/java/baseball/model/Position.java new file mode 100644 index 0000000000..8e9ee34d17 --- /dev/null +++ b/src/main/java/baseball/model/Position.java @@ -0,0 +1,42 @@ +package baseball.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Position { + public static final int LOWER_BOUNDS = 0; + public static final int UPPER_BOUNDS = 2; + private final int position; + + private static final Map CACHE = new HashMap<>(); + + static { + for (int position = LOWER_BOUNDS; position <= UPPER_BOUNDS; position++) { + CACHE.put(position, new Position(position)); + } + } + + private Position(int position) { + validate(position); + this.position = position; + } + + private void validate(int position) { + if (position < LOWER_BOUNDS || position > UPPER_BOUNDS) { + throw new IllegalArgumentException(); + } + } + + public static Position valueOf(int number) { + Position position = CACHE.get(number); + if (Objects.isNull(position)) { + position = new Position(number); + } + return position; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/baseball/view/InputValidator.java b/src/main/java/baseball/view/InputValidator.java new file mode 100644 index 0000000000..b1d0a35267 --- /dev/null +++ b/src/main/java/baseball/view/InputValidator.java @@ -0,0 +1,19 @@ +package baseball.view; + +public class InputValidator { + public static void validateNumbers(String numbers) { + try { + Integer.parseInt(numbers); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(); + } + } + + public static void validateCommandCode(String restart) { + try { + Integer.parseInt(restart); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 0000000000..aa1b4ec218 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,42 @@ +package baseball.view; + +import baseball.model.GameCommand; +import camp.nextstep.edu.missionutils.Console; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class InputView { + public static final String INPUT_NUMBERS_MESSAGE = "숫자를 입력해주세요 : "; + public static final String INPUT_GAME_COMMAND_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + private static InputView inputView; + + public static InputView getInstance() { + if (Objects.isNull(inputView)) { + inputView = new InputView(); + } + return inputView; + } + + public List inputNumbers() { + System.out.print(INPUT_NUMBERS_MESSAGE); + String numbers = Console.readLine(); + InputValidator.validateNumbers(numbers); + return toList(numbers); + } + + private static List toList(String numbers) { + return IntStream.range(0, numbers.length()) + .mapToObj(i -> Integer.parseInt(numbers.substring(i, i + 1))) + .collect(Collectors.toList()); + } + + public GameCommand inputGameCommand() { + System.out.println(INPUT_GAME_COMMAND_MESSAGE); + String commandCode = Console.readLine(); + InputValidator.validateCommandCode(commandCode); + return GameCommand.from(Integer.parseInt(commandCode)); + } +} diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 0000000000..f7ea90b222 --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,30 @@ +package baseball.view; + +import baseball.model.GameResult; + +import java.util.Objects; + +public class OutputView { + public static final String STARTING_MESSAGE = "숫자 야구 게임을 시작합니다."; + public static final String WINNING_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료"; + private static OutputView outputView; + + public static OutputView getInstance() { + if (Objects.isNull(outputView)) { + outputView = new OutputView(); + } + return outputView; + } + + public void printStartingMessage() { + System.out.println(STARTING_MESSAGE); + } + + public void printResult(GameResult gameResult) { + System.out.println(gameResult.getOutput()); + } + + public void printWinningMessage() { + System.out.println(WINNING_MESSAGE); + } +} diff --git a/src/test/java/baseball/model/BallNumberTest.java b/src/test/java/baseball/model/BallNumberTest.java new file mode 100644 index 0000000000..4bb1a5b218 --- /dev/null +++ b/src/test/java/baseball/model/BallNumberTest.java @@ -0,0 +1,22 @@ +package baseball.model; + +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 BallNumberTest { + @ParameterizedTest + @ValueSource(ints = {1, 2, 9}) + void valueOf(int ballNumber) { + assertThat(BallNumber.valueOf(ballNumber).getNumber()).isEqualTo(ballNumber); + } + + @ParameterizedTest + @ValueSource(ints = {0, 10}) + void valueOf_예외_던진다(int ballNumber) { + assertThatThrownBy(() -> BallNumber.valueOf(ballNumber)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/model/BallsTest.java b/src/test/java/baseball/model/BallsTest.java new file mode 100644 index 0000000000..d6e42c27ed --- /dev/null +++ b/src/test/java/baseball/model/BallsTest.java @@ -0,0 +1,50 @@ +package baseball.model; + +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.junit.jupiter.api.Assertions.*; + +class BallsTest { + + Balls computer = new Balls(List.of( + new Ball(BallNumber.valueOf(2), Position.valueOf(0)), + new Ball(BallNumber.valueOf(4), Position.valueOf(1)), + new Ball(BallNumber.valueOf(3), Position.valueOf(2)) + )); + + @ParameterizedTest + @MethodSource("userBallsAndGameResult") + void compare(Balls user, GameResult expected) { + assertThat(computer.compare(user)).isEqualTo(expected); + } + + static Stream userBallsAndGameResult() { + return Stream.of( + Arguments.of( + new Balls(List.of(new Ball(BallNumber.valueOf(2), Position.valueOf(0)), + new Ball(BallNumber.valueOf(4), Position.valueOf(1)), + new Ball(BallNumber.valueOf(3), Position.valueOf(2)))), + GameResult.of(0, 3) + ), + Arguments.of( + new Balls(List.of(new Ball(BallNumber.valueOf(2), Position.valueOf(0)), + new Ball(BallNumber.valueOf(3), Position.valueOf(1)), + new Ball(BallNumber.valueOf(7), Position.valueOf(2)))), + GameResult.of(1, 1) + ), + Arguments.of( + new Balls(List.of(new Ball(BallNumber.valueOf(1), Position.valueOf(0)), + new Ball(BallNumber.valueOf(7), Position.valueOf(1)), + new Ball(BallNumber.valueOf(5), Position.valueOf(2)))), + GameResult.of(0, 0) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/baseball/model/PositionTest.java b/src/test/java/baseball/model/PositionTest.java new file mode 100644 index 0000000000..7bd4884008 --- /dev/null +++ b/src/test/java/baseball/model/PositionTest.java @@ -0,0 +1,26 @@ +package baseball.model; + +import org.junit.jupiter.api.Test; +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.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PositionTest { + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void valueOf(int position) { + assertThat(Position.valueOf(position).getPosition()).isEqualTo(position); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 3}) + void valueOf_예외_던진다(int position) { + assertThatThrownBy(() -> Position.valueOf(position)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file