diff --git a/docs/README.md b/docs/README.md index e69de29bb2..9e5fed66b2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,37 @@ +# πŸ’ͺ ν”„λ‘œμ νŠΈ κ°œμš” + +컴퓨터가 μƒμ„±ν•œ 숫자λ₯Ό λ§žμΆ”κΈ° μœ„ν•΄ ν”Œλ ˆμ΄μ–΄κ°€ 숫자λ₯Ό μž…λ ₯ν•œλ‹€. +μ»΄ν“¨ν„°λŠ” ν”Œλ ˆμ΄μ–΄μ˜ μˆ«μžμ— λŒ€ν•΄ 슀트라이크, λ³Ό, λ‚«μ‹±μœΌλ‘œ 힌트λ₯Ό μ œκ³΅ν•œλ‹€. + +# πŸ“ κ΅¬ν˜„ κΈ°λŠ₯ λͺ©λ‘ + +### 컴퓨터가 숫자λ₯Ό μƒμ„±ν•˜λŠ” κΈ°λŠ₯ + +- [x] `숫자 야ꡬ κ²Œμž„μ„ μ‹œμž‘ν•©λ‹ˆλ‹€.`λ₯Ό 좜λ ₯ν•œλ‹€. +- [x] 1λΆ€ν„° 9κΉŒμ§€μ˜ 숫자 μ„Έ 개λ₯Ό μƒμ„±ν•œλ‹€. + +### ν”Œλ ˆμ΄μ–΄κ°€ 숫자λ₯Ό μž…λ ₯ν•˜λŠ” κΈ°λŠ₯ + +- [x] `숫자λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš” : `λ₯Ό 좜λ ₯ν•œλ‹€. +- [x] 숫자λ₯Ό μž…λ ₯λ°›λŠ”λ‹€. + - [x] 빈 λ¬Έμžμ—΄μ΄ μ•„λ‹˜μ„ κ²€μ¦ν•œλ‹€. + - [x] 길이가 3인 λ¬Έμžμ—΄μž„μ„ κ²€μ¦ν•œλ‹€. + - [x] 숫자둜 이루어진 λ¬Έμžμ—΄μž„μ„ κ²€μ¦ν•œλ‹€. + - [x] μˆ«μžκ°€ 1 이상, 9 μ΄ν•˜μž„μ„ κ²€μ¦ν•œλ‹€. + - [x] μ„œλ‘œ λ‹€λ₯Έ μˆ«μžμž„μ„ κ²€μ¦ν•œλ‹€. + +### 힌트λ₯Ό μƒμ„±ν•˜λŠ” κΈ°λŠ₯ + +- [x] 컴퓨터에 μ €μž₯ν•œ μˆ«μžμ™€ μž…λ ₯된 숫자λ₯Ό λΉ„κ΅ν•œλ‹€. +- [x] μŠ€νŠΈλΌμ΄ν¬μ™€ λ³Ό 개수λ₯Ό κ³„μ‚°ν•˜μ—¬ λ°˜ν™˜ν•œλ‹€. + +### 힌트λ₯Ό 좜λ ₯ν•˜λŠ” κΈ°λŠ₯ + +- [x] `1λ³Ό 1슀트라이크`와 같이 λ³Ό κ°œμˆ˜μ™€ 슀트라이크 개수λ₯Ό 좜λ ₯ν•œλ‹€. + +### κ²Œμž„μ„ μƒˆλ‘œ μ‹œμž‘ν•˜κ±°λ‚˜ μ’…λ£Œν•˜λŠ” κΈ°λŠ₯ + +- [x] μŠ€νŠΈλΌμ΄ν¬κ°€ 3인 경우 κ²Œμž„μ„ μ’…λ£Œν•œλ‹€. + - [x] `3개의 숫자λ₯Ό λͺ¨λ‘ λ§žνžˆμ…¨μŠ΅λ‹ˆλ‹€! κ²Œμž„ μ’…λ£Œ \n κ²Œμž„μ„ μƒˆλ‘œ μ‹œμž‘ν•˜λ €λ©΄ 1, μ’…λ£Œν•˜λ €λ©΄ 2λ₯Ό μž…λ ₯ν•˜μ„Έμš”.`λ₯Ό 좜λ ₯ν•œλ‹€. + - [x] 1 ν˜Ήμ€ 2의 μž…λ ₯을 λ°›λŠ”λ‹€. +- [x] μž…λ ₯에 따라 κ²Œμž„μ„ μƒˆλ‘œ μ‹œμž‘ν•˜κ±°λ‚˜ μ’…λ£Œν•œλ‹€. diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java index dd95a34214..bc41af4f62 100644 --- a/src/main/java/baseball/Application.java +++ b/src/main/java/baseball/Application.java @@ -1,7 +1,12 @@ package baseball; +import baseball.controller.GameManager; +import camp.nextstep.edu.missionutils.Console; + public class Application { public static void main(String[] args) { - // TODO: ν”„λ‘œκ·Έλž¨ κ΅¬ν˜„ + GameManager gameManager = new GameManager(); + gameManager.run(); + Console.close(); } } diff --git a/src/main/java/baseball/controller/GameManager.java b/src/main/java/baseball/controller/GameManager.java new file mode 100644 index 0000000000..9cac62f294 --- /dev/null +++ b/src/main/java/baseball/controller/GameManager.java @@ -0,0 +1,61 @@ +package baseball.controller; + +import baseball.domain.Computer; +import baseball.domain.HintResult; +import baseball.domain.Numbers; +import baseball.view.InputView; +import baseball.view.OutputView; +import baseball.view.console.ConsoleWriter; +import java.util.function.Supplier; + +public class GameManager { + private final InputView inputView; + private final OutputView outputView; + private final Computer computer; + + public GameManager() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.outputView.start(); + this.computer = new Computer(); + } + + public void run() { + boolean isRunning = true; + while (isRunning) { + computer.generate(); + isRunning = play(); + } + } + + private boolean play() { + while (true) { + Numbers numbers = retry(() -> { + return new Numbers(inputView.enterNumbers()); + }); + HintResult hintResult = computer.generateHintResult(numbers); + outputView.printHintResult(hintResult); + if (isSuccess(hintResult)) { + break; + } + } + outputView.printGameOver(); + return retry(() -> { + return inputView.enterRestartOrQuit().isRunning(); + }); + } + + private boolean isSuccess(HintResult hintResult) { + return hintResult.strike() == 3; + } + + private static T retry(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + ConsoleWriter.printlnMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/baseball/domain/Computer.java b/src/main/java/baseball/domain/Computer.java new file mode 100644 index 0000000000..b882906403 --- /dev/null +++ b/src/main/java/baseball/domain/Computer.java @@ -0,0 +1,38 @@ +package baseball.domain; + +import baseball.global.util.RandomNumberGenerator; +import java.util.ArrayList; +import java.util.List; + +public class Computer { + private List numbers; + + public Computer() { + this.numbers = new ArrayList<>(); + } + + public void generate() { + numbers = RandomNumberGenerator.generateRandomNumber(); + } + + public HintResult generateHintResult(Numbers givenNumbers) { + int strike = 0; + int ball = 0; + for (int i = 0; i < numbers.size(); i++) { + if (numbers.get(i) == givenNumbers.get(i)) { + strike++; + } + ball += isBallIndex(i, givenNumbers); + } + return new HintResult(strike, ball); + } + + private int isBallIndex(int i, Numbers givenNumbers) { + for (int j = 0; j < givenNumbers.size(); j++) { + if (i != j && numbers.get(i) == givenNumbers.get(j)) { + return 1; + } + } + return 0; + } +} diff --git a/src/main/java/baseball/domain/GameCommand.java b/src/main/java/baseball/domain/GameCommand.java new file mode 100644 index 0000000000..dbf9e1c678 --- /dev/null +++ b/src/main/java/baseball/domain/GameCommand.java @@ -0,0 +1,29 @@ +package baseball.domain; + +import baseball.global.exception.CustomException; +import baseball.global.exception.ErrorMessage; +import java.util.Arrays; + +public enum GameCommand { + RESTART("1", true), + QUIT("2", false); + + private final String command; + private final boolean isRunning; + + GameCommand(String command, boolean isRunning) { + this.command = command; + this.isRunning = isRunning; + } + + public static GameCommand from(String command) { + return Arrays.stream(GameCommand.values()) + .filter(element -> element.command.equals(command)) + .findFirst() + .orElseThrow(() -> CustomException.from(ErrorMessage.INVALID_NUMBER_ERROR)); + } + + public boolean isRunning() { + return isRunning; + } +} diff --git a/src/main/java/baseball/domain/HintResult.java b/src/main/java/baseball/domain/HintResult.java new file mode 100644 index 0000000000..f7ace8a492 --- /dev/null +++ b/src/main/java/baseball/domain/HintResult.java @@ -0,0 +1,7 @@ +package baseball.domain; + +public record HintResult( + int strike, + int ball +) { +} diff --git a/src/main/java/baseball/domain/Number.java b/src/main/java/baseball/domain/Number.java new file mode 100644 index 0000000000..dfdc1272cd --- /dev/null +++ b/src/main/java/baseball/domain/Number.java @@ -0,0 +1,24 @@ +package baseball.domain; + +import static baseball.global.validator.Validator.validateRange; + +public class Number { + private static final int START = 1; + private static final int END = 9; + private Integer value; + + public Number(Integer value) { + Validator.validate(value); + this.value = value; + } + + public int getValue() { + return value; + } + + private static class Validator { + public static void validate(int value) { + validateRange(value, 1, 9); + } + } +} diff --git a/src/main/java/baseball/domain/Numbers.java b/src/main/java/baseball/domain/Numbers.java new file mode 100644 index 0000000000..0a2e89fe64 --- /dev/null +++ b/src/main/java/baseball/domain/Numbers.java @@ -0,0 +1,54 @@ +package baseball.domain; + +import baseball.global.exception.CustomException; +import baseball.global.exception.ErrorMessage; +import java.util.List; + +public class Numbers { + private static final int COUNT = 3; + private List numbers; + + public Numbers(List numbers) { + this.numbers = Validator.validate(numbers); + } + + public int get(int index) { + return numbers.get(index).getValue(); + } + + public int size() { + return numbers.size(); + } + + private static class Validator { + private static List validate(List numbers) { + validateLength(numbers); + validateDuplicated(numbers); + return numbers.stream() + .map(Number::new) + .toList(); + } + + private static void validateLength(List numbers) { + if (numbers.size() != COUNT) { + throw CustomException.from(ErrorMessage.INVALID_NUMBER_ERROR); + } + } + + private static void validateDuplicated(List items) { + if (hasDuplicatedItem(items)) { + throw CustomException.from(ErrorMessage.INVALID_NUMBER_ERROR); + } + } + + private static boolean hasDuplicatedItem(List items) { + return items.size() != calculateUniqueItemsCount(items); + } + + private static int calculateUniqueItemsCount(List items) { + return (int) items.stream() + .distinct() + .count(); + } + } +} diff --git a/src/main/java/baseball/global/exception/CustomException.java b/src/main/java/baseball/global/exception/CustomException.java new file mode 100644 index 0000000000..138a9b9ee2 --- /dev/null +++ b/src/main/java/baseball/global/exception/CustomException.java @@ -0,0 +1,13 @@ +package baseball.global.exception; + +public class CustomException extends IllegalArgumentException { + private static final String PREFIX = "[ERROR] "; + + private CustomException(ErrorMessage errorMessage) { + super(PREFIX + errorMessage.getMessage()); + } + + public static CustomException from(ErrorMessage errorMessage) { + return new CustomException(errorMessage); + } +} diff --git a/src/main/java/baseball/global/exception/ErrorMessage.java b/src/main/java/baseball/global/exception/ErrorMessage.java new file mode 100644 index 0000000000..c683d8af32 --- /dev/null +++ b/src/main/java/baseball/global/exception/ErrorMessage.java @@ -0,0 +1,18 @@ +package baseball.global.exception; + +public enum ErrorMessage { + BLANK_INPUT_ERROR("빈 λ¬Έμžμ—΄μ΄ μž…λ ₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€."), + NOT_NUMBER_ERROR("μˆ«μžλ§Œμ„ μž…λ ₯ν•΄μ£Όμ„Έμš”."), + INVALID_NUMBER_ERROR("숫자λ₯Ό λ‹€μ‹œ μž…λ ₯ν•΄μ£Όμ„Έμš”."), + INVALID_RANGE_ERROR("μ˜¬λ°”λ₯Έ λ²”μœ„μ˜ 숫자λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/baseball/global/util/RandomNumberGenerator.java b/src/main/java/baseball/global/util/RandomNumberGenerator.java new file mode 100644 index 0000000000..8b9c48956e --- /dev/null +++ b/src/main/java/baseball/global/util/RandomNumberGenerator.java @@ -0,0 +1,18 @@ +package baseball.global.util; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.ArrayList; +import java.util.List; + +public class RandomNumberGenerator { + public static List generateRandomNumber() { + List computer = new ArrayList<>(); + while (computer.size() < 3) { + int randomNumber = Randoms.pickNumberInRange(1, 9); + if (!computer.contains(randomNumber)) { + computer.add(randomNumber); + } + } + return computer; + } +} diff --git a/src/main/java/baseball/global/validator/Validator.java b/src/main/java/baseball/global/validator/Validator.java new file mode 100644 index 0000000000..529e236c6e --- /dev/null +++ b/src/main/java/baseball/global/validator/Validator.java @@ -0,0 +1,27 @@ +package baseball.global.validator; + +import baseball.global.exception.CustomException; +import baseball.global.exception.ErrorMessage; + +public class Validator { + public static int validateNumber(String message) { + if (isNotNumber(message)) { + throw CustomException.from(ErrorMessage.NOT_NUMBER_ERROR); + } + return Integer.parseInt(message); + } + + private static boolean isNotNumber(String str) { + return !str.matches("-?\\d+"); + } + + public static void validateRange(int number, int start, int end) { + if (isInvalidRange(number, start, end)) { + throw CustomException.from(ErrorMessage.INVALID_RANGE_ERROR); + } + } + + private static boolean isInvalidRange(int number, int start, int end) { + return number < start || number > end; + } +} diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java new file mode 100644 index 0000000000..b75239d841 --- /dev/null +++ b/src/main/java/baseball/view/InputView.java @@ -0,0 +1,37 @@ +package baseball.view; + +import baseball.domain.GameCommand; +import baseball.view.console.ConsoleReader; +import baseball.view.console.ConsoleWriter; +import java.util.Arrays; +import java.util.List; + +public class InputView { + public List enterNumbers() { + ConsoleWriter.printMessage("숫자λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš” : "); + return Validator.validateNumbers(ConsoleReader.enterMessage()); + } + + public GameCommand enterRestartOrQuit() { + ConsoleWriter.printlnMessage("κ²Œμž„μ„ μƒˆλ‘œ μ‹œμž‘ν•˜λ €λ©΄ 1, μ’…λ£Œν•˜λ €λ©΄ 2λ₯Ό μž…λ ₯ν•˜μ„Έμš”."); + return GameCommand.from(ConsoleReader.enterMessage()); + } + + private static class Validator { + private static List validateNumbers(String message) { + List numbers = parseStringToList(message, ""); + + return numbers.stream() + .map(baseball.global.validator.Validator::validateNumber) + .toList(); + } + + private static List parseStringToList(String message, String separator) { + return Arrays.stream(split(message, separator)).toList(); + } + + private static String[] split(String message, String separator) { + return message.split(separator); + } + } +} diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java new file mode 100644 index 0000000000..8d132e9cec --- /dev/null +++ b/src/main/java/baseball/view/OutputView.java @@ -0,0 +1,43 @@ +package baseball.view; + +import baseball.domain.HintResult; +import baseball.view.console.ConsoleWriter; + +public class OutputView { + private static final String BALL = "%dλ³Ό"; + private static final String STRIKE = "%d슀트라이크"; + private static final String BOTH = "%s %s"; + private static final String NOTHING = "λ‚«μ‹±"; + + public void start() { + ConsoleWriter.printlnMessage("숫자 야ꡬ κ²Œμž„μ„ μ‹œμž‘ν•©λ‹ˆλ‹€."); + } + + public void printHintResult(HintResult hintResult) { + int strike = hintResult.strike(); + int ball = hintResult.ball(); + + ConsoleWriter.printlnMessage(generateResultMessage(strike, ball)); + } + + public String generateResultMessage(int strike, int ball) { + if (strike != 0 && ball != 0) { + return String.format( + BOTH, + String.format(BALL, ball), + String.format(STRIKE, strike) + ); + } + if (ball != 0) { + return String.format(BALL, ball); + } + if (strike != 0) { + return String.format(STRIKE, strike); + } + return NOTHING; + } + + public void printGameOver() { + ConsoleWriter.printlnMessage("3개의 숫자λ₯Ό λͺ¨λ‘ λ§žνžˆμ…¨μŠ΅λ‹ˆλ‹€! κ²Œμž„ μ’…λ£Œ"); + } +} diff --git a/src/main/java/baseball/view/console/ConsoleReader.java b/src/main/java/baseball/view/console/ConsoleReader.java new file mode 100644 index 0000000000..eecad34e25 --- /dev/null +++ b/src/main/java/baseball/view/console/ConsoleReader.java @@ -0,0 +1,24 @@ +package baseball.view.console; + +import baseball.global.exception.CustomException; +import baseball.global.exception.ErrorMessage; +import camp.nextstep.edu.missionutils.Console; + +public final class ConsoleReader { + public static String enterMessage() { + return Validator.validate(Console.readLine()); + } + + private static class Validator { + public static String validate(String message) { + validateBlankInput(message); + return message; + } + + private static void validateBlankInput(String message) { + if (message.isBlank()) { + throw CustomException.from(ErrorMessage.BLANK_INPUT_ERROR); + } + } + } +} diff --git a/src/main/java/baseball/view/console/ConsoleWriter.java b/src/main/java/baseball/view/console/ConsoleWriter.java new file mode 100644 index 0000000000..4e695bd82b --- /dev/null +++ b/src/main/java/baseball/view/console/ConsoleWriter.java @@ -0,0 +1,15 @@ +package baseball.view.console; + +public final class ConsoleWriter { + public static void printMessage(String message) { + System.out.print(message); + } + + public static void printlnMessage(String message) { + System.out.println(message); + } + + public static void printlnFormat(String message, Object... args) { + printlnMessage(String.format(message, args)); + } +}