diff --git a/README.md b/README.md index 8fe7112..3260f35 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,38 @@ ## [NEXTSTEP 플레이그라운드의 미션 진행 과정](https://github.com/next-step/nextstep-docs/blob/master/playground/README.md) --- -## 학습 효과를 높이기 위해 추천하는 미션 진행 방법 +## 목표 +- 객체에게 적절한 책임을 분배한다. +- 객체간에 메시지를 통해서만 서로 소통한다. +- TDD 개발 방법을 경험한다. ---- -1. 피드백 강의 전까지 미션 진행 -> 피드백 강의 전까지 혼자 힘으로 미션 진행. 미션을 진행하면서 하나의 작업이 끝날 때 마다 add, commit -> 예를 들어 다음 숫자 야구 게임의 경우 0, 1, 2단계까지 구현을 완료한 후 push - -![mission baseball](https://raw.githubusercontent.com/next-step/nextstep-docs/master/playground/images/mission_baseball.png) - ---- -2. 피드백 앞 단계까지 미션 구현을 완료한 후 피드백 강의를 학습한다. - ---- -3. Git 브랜치를 master 또는 main으로 변경한 후 피드백을 반영하기 위한 새로운 브랜치를 생성한 후 처음부터 다시 미션 구현을 도전한다. +## 요구사항 +- 1 ~ 9까지 서로 다른 수로 이루어진 세자리 수를 맞추는 게임이다. +- 3 스트라이크를 달성하면 게임이 종료된다. + - 위치, 숫자 모두 맞춘경우 -> 스트라이크 + - 위치는 틀리고 숫자만 맞춘 경우 -> 볼 + - 위치, 숫자 모두 틀린 경우 -> 낫싱 +- 게임을 종료 한 후 게임을 다시 시작하거나 완전히 종료 할 수 있다. -``` -git branch -a // 모든 로컬 브랜치 확인 -git checkout master // 기본 브랜치가 master인 경우 -git checkout main // 기본 브랜치가 main인 경우 +## 구현사항 +- 컴퓨터는 랜덤 숫자 3개를 입력한다. + - 1 ~ 9 까지의 서로 다른 숫자이다. +- 플레이어는 숫자를 입력한다. + - 1 ~ 9까지의 서로 다른 숫자이다. +- 입력받은 숫자를 비교한다. + - 위치, 숫자 모두 맞춘경우 -> 스트라이크 + - 위치는 틀리고 숫자만 맞춘 경우 -> 볼 + - 위치, 숫자 모두 틀린 경우 -> 낫싱 +- 결과를 출력한다. +- 결과에 따라 UI를 분기한다. + - 3 스트라이크가 아니라면? -> 게임 재시작 + - 3 스트라이크라면? -> 새로 시작 Or 게임 종료 선택 UI + +## 객체 +- RandomNumberGenerator : 1 ~ 9 까지의 RandomNumber를 생성한다. +- InputNumber : 입력값의 정합성을 보장한다. ( 1 ~ 9 까지의 숫자 ) +- InputNumbers : 입력값의 정합성을 보장한다. ( 서로 다른 숫자 ) +- Game : 게임의 진행을 담당한다. +- Input : 게임 진행중 모든 입력을 담당한다. +- Output : 게임 진행중 모든 출력을 담당한다. -git checkout -b 브랜치이름 -ex) git checkout -b apply-feedback -``` diff --git a/src/main/java/Ball.java b/src/main/java/Ball.java new file mode 100644 index 0000000..d3b5bda --- /dev/null +++ b/src/main/java/Ball.java @@ -0,0 +1,50 @@ +import java.util.Objects; + +public class Ball { + + private final int position; + + private final int number; + + public Ball(int position, int number) { + this.position = position; + this.number = number; + } + + public int getPosition() { + return position; + } + + public int getNumber() { + return number; + } + + public BallStatus compare(Ball userBall) { + if (this.equals(userBall)) { + return BallStatus.STRIKE; + } + + if (position != userBall.getPosition() && number == userBall.getNumber()) { + return BallStatus.BALL; + } + + return BallStatus.NOTHING; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Ball ball = (Ball) o; + return position == ball.position && number == ball.number; + } + + @Override + public int hashCode() { + return Objects.hash(position, number); + } +} diff --git a/src/main/java/BallStatus.java b/src/main/java/BallStatus.java new file mode 100644 index 0000000..1ec5a71 --- /dev/null +++ b/src/main/java/BallStatus.java @@ -0,0 +1,17 @@ +public enum BallStatus { + + STRIKE, BALL, NOTHING; + + public static boolean isNotNothing(BallStatus status) { + return status != BallStatus.NOTHING; + } + + public static boolean isStrike(BallStatus status) { + return status == BallStatus.STRIKE; + } + + public static boolean isBall(BallStatus status) { + return status == BallStatus.BALL; + } + +} diff --git a/src/main/java/Balls.java b/src/main/java/Balls.java new file mode 100644 index 0000000..f914b43 --- /dev/null +++ b/src/main/java/Balls.java @@ -0,0 +1,74 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Balls { + + private final List balls; + + public Balls(InputNumbers inputNumbers) { + balls = new ArrayList<>(); + + validateInputNumbers(inputNumbers); + convertToBalls(inputNumbers); + } + + public List getBalls() { + return balls; + } + + private void validateInputNumbers(InputNumbers inputNumbers) { + if (Objects.isNull(inputNumbers)) { + throw new IllegalArgumentException("입력값이 없습니다."); + } + } + + private void convertToBalls(InputNumbers inputNumbers) { + List inputNumberList = inputNumbers.getInputNumbers(); + for (int i = 0; i < inputNumberList.size(); i++) { + balls.add(new Ball(i, inputNumberList.get(i).getNumber())); + } + } + + public Score play(InputNumbers userInputNumbers) { + Score score = new Score(); + + Balls userBalls = new Balls(userInputNumbers); + if (this.balls.equals(userBalls)) { + return new Score(3, 0); + } + + for (Ball userBall: userBalls.getBalls()) { + BallStatus status = compareWithUserBall(userBall); + score.calculate(status); + } + + return score; + } + + private BallStatus compareWithUserBall(Ball userBall) { + return balls.stream() + .map(ball -> ball.compare(userBall)) + .filter(BallStatus::isNotNothing) + .findFirst() + .orElse(BallStatus.NOTHING); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Balls balls1 = (Balls) o; + return balls.equals(balls1.balls); + } + + @Override + public int hashCode() { + return Objects.hash(balls); + } + +} diff --git a/src/main/java/ConfirmCommand.java b/src/main/java/ConfirmCommand.java new file mode 100644 index 0000000..8e6eee5 --- /dev/null +++ b/src/main/java/ConfirmCommand.java @@ -0,0 +1,28 @@ +import java.util.Arrays; + +public enum ConfirmCommand { + CONTINUE(1), + EXIT(2); + + int value; + + ConfirmCommand(int value) { + this.value = value; + } + + public static ConfirmCommand findConfirmCommand(String input) { + return Arrays.stream(ConfirmCommand.values()) + .filter(confirmCommand -> confirmCommand.getValue() == Integer.parseInt(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 커맨드입니다.")); + } + + public int getValue() { + return value; + } + + public boolean isExit() { + return this.equals(ConfirmCommand.EXIT); + } + +} diff --git a/src/main/java/ConsoleInput.java b/src/main/java/ConsoleInput.java new file mode 100644 index 0000000..6c004df --- /dev/null +++ b/src/main/java/ConsoleInput.java @@ -0,0 +1,19 @@ +import java.util.Scanner; + +public class ConsoleInput extends GameInput{ + + public static final String REQUEST_INPUT_NUMBERS_MESSAGE = "숫자를 입력해 주세요. : "; + + private Scanner scanner; + + public ConsoleInput() { + this.scanner = new Scanner(System.in); + } + + @Override + public String input() { + System.out.print(REQUEST_INPUT_NUMBERS_MESSAGE); + return scanner.nextLine(); + } + +} diff --git a/src/main/java/ConsoleOutput.java b/src/main/java/ConsoleOutput.java new file mode 100644 index 0000000..5732cf9 --- /dev/null +++ b/src/main/java/ConsoleOutput.java @@ -0,0 +1,8 @@ +public class ConsoleOutput extends GameOutput { + + @Override + protected void output(String message) { + System.out.println(message); + } + +} diff --git a/src/main/java/Game.java b/src/main/java/Game.java new file mode 100644 index 0000000..57fb948 --- /dev/null +++ b/src/main/java/Game.java @@ -0,0 +1,60 @@ +public class Game { + + public static final int MAX_NUMBER_COUNT = 3; + private final RandomNumberGenerator generator; + + private final GameInput input; + + private final GameOutput output; + + public Game(RandomNumberGenerator generator, GameInput input, GameOutput output) { + this.generator = generator; + this.input = input; + this.output = output; + } + + public void start() { + + // 랜덤 숫자 3개 입력받기 By Computer + InputNumbers computerInputNumbers = getRandomNumbers(); + + while (true) { + // 숫자 3개를 입력받는다. By User + InputNumbers userInputNumbers = input.getUserInputNumbers(); + + // 입력받은 두 값을 비교해서 점수를 계산한다. + Balls computerBalls = new Balls(computerInputNumbers); + Score score = computerBalls.play(userInputNumbers); + + // 결과를 출력한다. + output.output(score.makeResultMessage()); + + // 결과에 따라 UI를 이동한다. + if (score.getStrikeCount() != 3) { + continue; + } + + output.confirmMessage(); + + ConfirmCommand confirmCommand = input.inputConfirmCommand(); + if (confirmCommand.isExit()) { + return; + } + + start(); // 재귀사용 + return; + } + + } + + private InputNumbers getRandomNumbers() { + InputNumbers inputNumbers = new InputNumbers(); + while (inputNumbers.getInputNumbers().size() < MAX_NUMBER_COUNT) { + try { + inputNumbers.addInputNumber(generator.generate()); + } catch (IllegalArgumentException e) { } + } + return inputNumbers; + } + +} diff --git a/src/main/java/GameApplication.java b/src/main/java/GameApplication.java new file mode 100644 index 0000000..97773d2 --- /dev/null +++ b/src/main/java/GameApplication.java @@ -0,0 +1,12 @@ +public class GameApplication { + + public static void main(String[] args) { + GameInput consoleInput = new ConsoleInput(); + GameOutput consoleOutput = new ConsoleOutput(); + RandomNumberGenerator generator = new RandomNumberGenerator(); + + Game game = new Game(generator, consoleInput, consoleOutput); + game.start(); + } + +} diff --git a/src/main/java/GameInput.java b/src/main/java/GameInput.java new file mode 100644 index 0000000..61f7e04 --- /dev/null +++ b/src/main/java/GameInput.java @@ -0,0 +1,47 @@ +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public abstract class GameInput { + + public static final String REQUEST_RE_INPUT_MESSAGE = "다시 입력해주세요."; + + public InputNumbers getUserInputNumbers() { + + String inputStr = inputStr = input(); + List inputs = convertToIntegerList(inputStr); + + InputNumbers inputNumbers = null; + try { + inputNumbers = new InputNumbers(inputs); + + } catch (IllegalArgumentException e) { + System.out.println(REQUEST_RE_INPUT_MESSAGE); + getUserInputNumbers(); + } + + return inputNumbers; + } + + public ConfirmCommand inputConfirmCommand() { + ConfirmCommand confirmCommand = null; + + try { + confirmCommand = ConfirmCommand.findConfirmCommand(input()); + } catch (IllegalArgumentException e) { + System.out.println(REQUEST_RE_INPUT_MESSAGE); + inputConfirmCommand(); + } + + return confirmCommand; + } + + private List convertToIntegerList(String inputStr) { + return Arrays.stream(inputStr.split("")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + protected abstract String input(); + +} diff --git a/src/main/java/GameOutput.java b/src/main/java/GameOutput.java new file mode 100644 index 0000000..d1da1e3 --- /dev/null +++ b/src/main/java/GameOutput.java @@ -0,0 +1,11 @@ +public abstract class GameOutput { + + public static final String GAME_EXIT_OR_CONTINUE_CONFIRM_MESSAGE = "3개의 숫자를 모두 맞히셨습니다! 게임 종료\n 게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + + public void confirmMessage() { + output(GAME_EXIT_OR_CONTINUE_CONFIRM_MESSAGE); + } + + protected abstract void output(String message); + +} diff --git a/src/main/java/InputNumber.java b/src/main/java/InputNumber.java new file mode 100644 index 0000000..0ef679a --- /dev/null +++ b/src/main/java/InputNumber.java @@ -0,0 +1,24 @@ +public class InputNumber { + + private final int MAX = 9; + + private final int MIN = 1; + + private final int number; + + public InputNumber(int number) { + validateNumberRange(number); + this.number = number; + } + + public int getNumber() { + return number; + } + + private void validateNumberRange(int number) { + if (number > MAX || number < MIN) { + throw new IllegalArgumentException("입력된 숫자가 유효하지 않습니다."); + } + } + +} diff --git a/src/main/java/InputNumbers.java b/src/main/java/InputNumbers.java new file mode 100644 index 0000000..9c0ff62 --- /dev/null +++ b/src/main/java/InputNumbers.java @@ -0,0 +1,41 @@ +import java.util.ArrayList; +import java.util.List; + +public class InputNumbers { + + private final List inputNumbers; + + public InputNumbers() { + inputNumbers = new ArrayList<>(); + } + + public InputNumbers(List numbers) { + inputNumbers = new ArrayList<>(); + numbers.forEach(this::addInputNumber); + } + + // TODO : getter가 있어도 될까? + public List getInputNumbers() { + return inputNumbers; + } + + public void addInputNumber(int number) { + validateDuplicatedInputNumber(number); + inputNumbers.add(new InputNumber(number)); + } + + private void validateDuplicatedInputNumber(int number) { + if (inputNumbers.isEmpty()) { + return; + } + + boolean isDuplicated = inputNumbers.stream() + .anyMatch(inputNumber -> inputNumber.getNumber() == number); + + if (isDuplicated) { + throw new IllegalArgumentException("중복된 숫자가 존재합니다."); + } + } + + +} diff --git a/src/main/java/RandomNumberGenerator.java b/src/main/java/RandomNumberGenerator.java new file mode 100644 index 0000000..5739112 --- /dev/null +++ b/src/main/java/RandomNumberGenerator.java @@ -0,0 +1,19 @@ +import java.util.Random; + +public class RandomNumberGenerator { + + private final int MAX = 9; + + private final int MIN = 1; + + private final Random random; + + public RandomNumberGenerator() { + this.random = new Random(); + } + + public int generate() { + return random.nextInt((MAX - MIN) + 1) + MIN; + } + +} diff --git a/src/main/java/Score.java b/src/main/java/Score.java new file mode 100644 index 0000000..41e837c --- /dev/null +++ b/src/main/java/Score.java @@ -0,0 +1,57 @@ +public class Score { + + private int strikeCount; + + private int ballCount; + + public Score() { + this.strikeCount = 0; + this.ballCount = 0; + } + + public Score(int strikeCount, int ballCount) { + this.strikeCount = strikeCount; + this.ballCount = ballCount; + } + + public int getStrikeCount() { + return strikeCount; + } + + public int getBallCount() { + return ballCount; + } + + public void calculate(BallStatus status) { + if (BallStatus.isStrike(status)) { + plusStrikeCount(); + } + + if (BallStatus.isBall(status)) { + plusBallCount(); + } + } + + public String makeResultMessage() { + StringBuilder stringBuilder = new StringBuilder(); + + if (ballCount > 0) { + stringBuilder.append(ballCount + "Ball "); + } + + if (strikeCount > 0) { + stringBuilder.append(strikeCount + "Strike"); + } + + return stringBuilder.toString(); + } + + private void plusStrikeCount() { + strikeCount++; + } + + private void plusBallCount() { + ballCount++; + } + +} diff --git a/src/test/java/BallTest.java b/src/test/java/BallTest.java new file mode 100644 index 0000000..9a1f5cc --- /dev/null +++ b/src/test/java/BallTest.java @@ -0,0 +1,37 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class BallTest { + + @Test + void test_nothing() { + Ball userBall = new Ball(1,3); + Ball computerBall = new Ball(1,2); + + BallStatus status = computerBall.compare(userBall); + + assertThat(status).isEqualTo(BallStatus.NOTHING); + } + + @Test + void test_strike() { + Ball userBall = new Ball(1,3); + Ball computerBall = new Ball(1,3); + + BallStatus status = computerBall.compare(userBall); + + assertThat(status).isEqualTo(BallStatus.STRIKE); + } + + @Test + void test_ball() { + Ball userBall = new Ball(1,3); + Ball computerBall = new Ball(2,3); + + BallStatus status = computerBall.compare(userBall); + + assertThat(status).isEqualTo(BallStatus.BALL); + } + +} diff --git a/src/test/java/BallsTest.java b/src/test/java/BallsTest.java new file mode 100644 index 0000000..c3deaee --- /dev/null +++ b/src/test/java/BallsTest.java @@ -0,0 +1,57 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BallsTest { + + private final List COMPUTER_INPUT_NUMBERS = List.of(1, 2, 3); + private Balls computerBalls; + + @BeforeEach + void setUp() { + InputNumbers inputNumbers = new InputNumbers(); + COMPUTER_INPUT_NUMBERS.forEach(inputNumbers::addInputNumber); + + computerBalls = new Balls(inputNumbers); + } + + @Test + void test_3strike() { + InputNumbers inputNumbers = new InputNumbers(); + for (Integer computerInputNumber : COMPUTER_INPUT_NUMBERS) { + inputNumbers.addInputNumber(computerInputNumber); + } + + Score userScore = computerBalls.play(inputNumbers); + + assertThat(userScore.getStrikeCount()).isEqualTo(3); + assertThat(userScore.getBallCount()).isEqualTo(0); + } + + @Test + void test_1strike_1ball() { + List userInputNumberList = List.of(1, 5, 2); + InputNumbers inputNumbers = new InputNumbers(); + userInputNumberList.forEach(inputNumbers::addInputNumber); + + Score userScore = computerBalls.play(inputNumbers); + + assertThat(userScore.getStrikeCount()).isEqualTo(1); + assertThat(userScore.getBallCount()).isEqualTo(1); + } + + @Test + void test_nothing() { + List userInputNumberList = List.of(4, 5, 6); + InputNumbers inputNumbers = new InputNumbers(); + userInputNumberList.forEach(inputNumbers::addInputNumber); + + Score userScore = computerBalls.play(inputNumbers); + + assertThat(userScore.getStrikeCount()).isEqualTo(0); + assertThat(userScore.getBallCount()).isEqualTo(0); + } + +} diff --git a/src/test/java/InputNumberTest.java b/src/test/java/InputNumberTest.java new file mode 100644 index 0000000..bdcefc1 --- /dev/null +++ b/src/test/java/InputNumberTest.java @@ -0,0 +1,24 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +public class InputNumberTest { + + @Test + void test_constructor_success() { + int validNumber = 1; + InputNumber inputNumber = new InputNumber(validNumber); + + assertThat(inputNumber).isNotNull(); + } + + @Test + void test_constructor_range_over() { + int rangeOverNumber = 10; + + assertThatThrownBy(() -> new InputNumber(rangeOverNumber)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/InputNumbersTest.java b/src/test/java/InputNumbersTest.java new file mode 100644 index 0000000..6a664b5 --- /dev/null +++ b/src/test/java/InputNumbersTest.java @@ -0,0 +1,39 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InputNumbersTest { + + private InputNumbers inputNumbers; + + @BeforeEach + void setUp() { + inputNumbers = new InputNumbers(); + } + + @Test + void test_addInputNumber_success() { + int validNumber = 1; + + inputNumbers.addInputNumber(validNumber); + + assertThat(inputNumbers.getInputNumbers().size()).isEqualTo(1); + // TODO: 매우 찜찜하다.. 테스트를 위해서 이렇게 까지 타고 들어가는게 맞을까?? + assertThat(inputNumbers.getInputNumbers().get(0).getNumber()).isEqualTo(validNumber); + } + + @Test + void test_addInputNumber_duplicated_number() { + int validNumber = 1; + int duplicatedNumber = validNumber; + + inputNumbers.addInputNumber(validNumber); + + assertThatThrownBy(() -> inputNumbers.addInputNumber(duplicatedNumber)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/RandomNumberGeneratorTest.java b/src/test/java/RandomNumberGeneratorTest.java new file mode 100644 index 0000000..c8537a5 --- /dev/null +++ b/src/test/java/RandomNumberGeneratorTest.java @@ -0,0 +1,26 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RandomNumberGeneratorTest { + + private final int MAX = 9; + + private final int MIN = 1; + + private RandomNumberGenerator generator; + + @BeforeEach + void setUp() { + generator = new RandomNumberGenerator(); + } + + @Test + void test_generate() { + int randomNumber = generator.generate(); + assertThat(randomNumber).isBetween(MIN, MAX); + assertThat(randomNumber).isNotNull(); + } + +} diff --git a/src/test/java/ScoreTest.java b/src/test/java/ScoreTest.java new file mode 100644 index 0000000..18fcda8 --- /dev/null +++ b/src/test/java/ScoreTest.java @@ -0,0 +1,40 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScoreTest { + + private Score score; + + @BeforeEach + void setUp() { + score = new Score(); + } + + @Test + void test_calculate_strike() { + score.calculate(BallStatus.STRIKE); + + assertThat(score.getStrikeCount()).isEqualTo(1); + assertThat(score.getBallCount()).isEqualTo(0); + } + + @Test + void test_calculate_ball() { + score.calculate(BallStatus.BALL); + + assertThat(score.getStrikeCount()).isEqualTo(0); + assertThat(score.getBallCount()).isEqualTo(1); + } + + @Test + void test_calculate_nothing() { + score.calculate(BallStatus.NOTHING); + + assertThat(score.getStrikeCount()).isEqualTo(0); + assertThat(score.getBallCount()).isEqualTo(0); + } + +} diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java deleted file mode 100644 index 43e47d9..0000000 --- a/src/test/java/study/StringTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package study; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class StringTest { - @Test - void replace() { - String actual = "abc".replace("b", "d"); - assertThat(actual).isEqualTo("adc"); - } -}