diff --git a/README.md b/README.md index 8d7e8aee..1046b8a0 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +## 구현할 기능 목록 + +- 게임이 실행되고, 컴퓨터가 생각한 수를 사전에 미리 설정할 수 있는 기능 구현 +- 사용자에게 안내 문구를 출력한 뒤 입력을 받아오는 기능 구현 +- 사용자의 입력값이 잘못 되었을 경우 에러를 출력하는 기능 구현 +- 입력값을 토대로 결과를 판단하는 심판 기능 구현 +- 결과를 출력하는 기능 구현 +- 게임 진행 흐름을 컨트롤 하는 기능 구현 \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..6f9cdc67 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,14 @@ +import controller.GameController; +import view.InputView; +import view.OutputView; + +public class Application { + + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + GameController gameController = new GameController(inputView, outputView); + gameController.start(); + } + +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000..d7838de2 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,68 @@ +package controller; + +import model.Referee; +import model.player.ComputerPlayer; +import model.player.NormalPlayer; +import view.InputView; +import view.OutputView; + +public class GameController { + + private final InputView inputView; + private final OutputView outputView; + + public GameController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void start() { + while (true) { + NormalPlayer player = new NormalPlayer(); + ComputerPlayer computerPlayer = new ComputerPlayer(); + play(player, computerPlayer); + outputView.printFinishMessage(); + int validResume = getValidResume(); + if (validResume != 1) { + return; + } + } + } + + private void play(NormalPlayer player, ComputerPlayer computerPlayer) { + computerPlayer.setRandomNumbers(); + Referee referee = new Referee(); + + boolean gameOver = false; + + while (!gameOver) { + player.updateNumbers(getValidUserNumbers()); + int[] score = referee.judgeGameScore(player.getNumbers(), computerPlayer.getNumbers()); + outputView.printResultMessage(score[0], score[1]); + gameOver = referee.judgeGameOver(score); + } + + } + + private int[] getValidUserNumbers() { + while (true) { + try { + outputView.printInputMessage(); + return inputView.getUserNumbers(); + } catch (Exception e) { + outputView.printlnMessage(e.getMessage()); + } + } + } + + private int getValidResume() { + while (true) { + try { + outputView.printResumeMessage(); + return inputView.getResumeOption(); + } catch (Exception e) { + outputView.printlnMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/model/Referee.java b/src/main/java/model/Referee.java new file mode 100644 index 00000000..d2187b60 --- /dev/null +++ b/src/main/java/model/Referee.java @@ -0,0 +1,41 @@ +package model; + +public class Referee { + private static int OFFSET = 3; + + public int[] judgeGameScore(int[] userNumberArray, int[] computerNumberArray) { + int[] score = {0, 0}; // ball, strike + score[0] = countBall(userNumberArray, computerNumberArray); + score[1] = countStrike(userNumberArray, computerNumberArray); + return score; + } + + public boolean judgeGameOver(int[] score) { + return score[1] == OFFSET; + } + + private int countStrike(int[] userNumberArray, int[] computerNumberArray) { + int cnt = 0; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (userNumberArray[i] == computerNumberArray[j] && i == j) { + cnt++; + + } + } + } + return cnt; + } + + private int countBall(int[] userNumberArray, int[] computerNumberArray) { + int cnt = 0; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (userNumberArray[i] == computerNumberArray[j] && i != j) { + cnt++; + } + } + } + return cnt; + } +} \ No newline at end of file diff --git a/src/main/java/model/player/ComputerPlayer.java b/src/main/java/model/player/ComputerPlayer.java new file mode 100644 index 00000000..9ace0fc4 --- /dev/null +++ b/src/main/java/model/player/ComputerPlayer.java @@ -0,0 +1,39 @@ +package model.player; + +import java.util.Random; + +public class ComputerPlayer { + + private final int[] numbers; + + public ComputerPlayer() { + this.numbers = new int[3]; + } + + public int[] getNumbers() { + return numbers; + } + + public void setRandomNumbers() { + Random random = new Random(); + int idx = 0; + + while (idx < 3) { + int randomNumber = random.nextInt(9) + 1; + + if (!isDuplicate(numbers, randomNumber, idx)) { + numbers[idx] = randomNumber; + idx++; + } + } + } + + private boolean isDuplicate(int[] arr, int num, int idx) { + for (int i = 0; i < idx; i++) { + if (arr[i] == num) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/model/player/NormalPlayer.java b/src/main/java/model/player/NormalPlayer.java new file mode 100644 index 00000000..26ad4ac9 --- /dev/null +++ b/src/main/java/model/player/NormalPlayer.java @@ -0,0 +1,20 @@ +package model.player; + +public class NormalPlayer { + + private final int[] numbers; + + public NormalPlayer() { + this.numbers = new int[3]; + } + + public int[] getNumbers() { + return numbers; + } + + public void updateNumbers(int[] inputNumbers) { + this.numbers[0] = inputNumbers[0]; + this.numbers[1] = inputNumbers[1]; + this.numbers[2] = inputNumbers[2]; + } +} diff --git a/src/main/java/util/StringUtils.java b/src/main/java/util/StringUtils.java new file mode 100644 index 00000000..57f213c9 --- /dev/null +++ b/src/main/java/util/StringUtils.java @@ -0,0 +1,13 @@ +package util; + +public class StringUtils { + + public static int[] parseStringToIntArray(String inputString) { + String[] array = inputString.split(""); + int[] numberArray = new int[3]; + for (int i = 0; i < 3; i++) { + numberArray[i] = Integer.parseInt(array[i]); + } + return numberArray; + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..67d61c0d --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,91 @@ +package view; + +import util.StringUtils; + +import java.io.BufferedReader; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class InputView { + + private final BufferedReader reader; + + public InputView() { + this.reader = new BufferedReader(new InputStreamReader(System.in)); + } + + public int[] getUserNumbers() throws Exception { + String inputString = reader.readLine(); + checkInputFormat(inputString); + return StringUtils.parseStringToIntArray(inputString); + } + + public int getResumeOption() throws Exception { + String inputString = reader.readLine(); + checkResumeInputFormat(inputString); + return Integer.parseInt(inputString); + } + + private void checkInputFormat(String inputString) throws Exception { + validateIntegerInput(inputString); + validateIntegerLength(inputString, 3); + validateNotZero(inputString); + validateDuplicatedNumber(inputString); + } + + public void checkResumeInputFormat(String inputString) throws Exception{ + validateIntegerInput(inputString); + validateIntegerLength(inputString, 1); + validateResumeFormat(inputString); + } + + private void validateIntegerInput(String inputString) throws Exception{ + if (!isInteger(inputString)) { + throw new IllegalArgumentException("[ERROR] 숫자만 입력 가능합니다."); + } + } + + private boolean isInteger(String inputString) throws Exception { + try { + Integer.parseInt(inputString); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + private void validateIntegerLength(String inputString, int N) throws Exception { + if (inputString.length() != N) { + throw new IllegalArgumentException("[ERROR] " + N + "자리의 숫자여야 합니다."); + } + } + + private void validateNotZero(String inputString) throws Exception { + if (inputString.contains("0")) { + throw new IllegalArgumentException("[ERROR] 0은 포함될 수 없습니다."); + } + } + + private void validateDuplicatedNumber(String inputString) throws Exception { + String[] array = inputString.split(""); + Set set = new HashSet<>(Arrays.asList(array)); + if (set.size() != array.length) { + throw new IllegalArgumentException("[ERROR] 중복된 숫자가 있습니다."); + } + } + + private void validateResumeFormat(String inputString) throws Exception { + if (!inputString.equals("1") && !inputString.equals("2")) { + throw new IllegalArgumentException("[ERROR] 1 또는 2만 입력 가능합니다."); + } + } + + public void close() throws IOException { + reader.close(); + } + +} \ No newline at end of file diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..22a67f5d --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,47 @@ +package view; + +public class OutputView { + + private static final String inputMessage = "숫자를 입력해주세요 : "; + private static final String ballMessage = "볼 "; + private static final String strikeMessage = "스트라이크"; + private static final String nothingMessage = "낫싱"; + private static final String endMessage = "3개의 숫자를 모두 맞히셨습니다! 게임 끝"; + private static final String resumeMessage = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + + + public void printlnMessage(String message) { + System.out.println(message); + } + + public void printMessage(String message) { + System.out.print(message); + } + + public void printInputMessage() { + printMessage(inputMessage); + } + + public void printFinishMessage() { + printlnMessage(endMessage); + } + + public void printResumeMessage() { + printlnMessage(resumeMessage); + } + + public void printResultMessage(int ball, int strike) { + String message = ""; + if (strike == 0 && ball == 0) { + message += nothingMessage; + } + if (ball > 0) { + message += ball + ballMessage; + } + if (strike > 0) { + message += strike + strikeMessage; + } + printlnMessage(message); + } + +} diff --git a/src/test/java/model/RefereeTest.java b/src/test/java/model/RefereeTest.java new file mode 100644 index 00000000..193057d4 --- /dev/null +++ b/src/test/java/model/RefereeTest.java @@ -0,0 +1,57 @@ +package model; + +import static org.junit.jupiter.api.Assertions.*; + +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.CsvSource; + +class RefereeTest { + + private Referee referee; + private final int[] computerNumbers = {1, 2, 3}; + + @BeforeEach + void setUp() { + referee = new Referee(); + } + + @ParameterizedTest + @CsvSource({ + "1, 2, 3, 0, 3", + "3, 1, 2, 3, 0", + "1, 3, 2, 2, 1", + "1, 4, 5, 0, 1", + "4, 5, 6, 0, 0", + "2, 3, 1, 3, 0" + }) + @DisplayName("사용자 입력에 따른 볼과 스트라이크 개수를 정확히 계산해야 한다") + void testJudgeGameScore(int n1, int n2, int n3, int expectedBall, int expectedStrike) { + int[] userNumbers = {n1, n2, n3}; + + int[] score = referee.judgeGameScore(userNumbers, computerNumbers); + + assertAll( + () -> assertEquals(expectedBall, score[0], "볼 개수가 일치하지 않습니다."), + () -> assertEquals(expectedStrike, score[1], "스트라이크 개수가 일치하지 않습니다.") + ); + } + + @Test + @DisplayName("3스트라이크일 경우 게임 종료 판정(true)을 내려야 한다") + void testJudgeGameOver_True() { + int[] score = {0, 3}; + + assertTrue(referee.judgeGameOver(score)); + } + + @Test + @DisplayName("3스트라이크가 아닐 경우 게임 종료 판정(false)을 내려야 한다") + void testJudgeGameOver_False() { + int[] score = {2, 1}; + + assertFalse(referee.judgeGameOver(score)); + } +} \ No newline at end of file diff --git a/src/test/java/model/player/ComputerPlayerTest.java b/src/test/java/model/player/ComputerPlayerTest.java new file mode 100644 index 00000000..2bde18fc --- /dev/null +++ b/src/test/java/model/player/ComputerPlayerTest.java @@ -0,0 +1,46 @@ +package model.player; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ComputerPlayerTest { + + @Test + @DisplayName("생성된 모든 숫자는 1 이상 9 이하의 정수여야 한다") + void testNumbersWithinRange() { + ComputerPlayer player = new ComputerPlayer(); + + for (int i = 0; i < 100; i++) { + player.setRandomNumbers(); + int[] numbers = player.getNumbers(); + + for (int num : numbers) { + assertTrue(num >= 1 && num <= 9, + "1~9의 범위를 벗어난 숫자가 존재합니다: " + num); + } + } + } + + @Test + @DisplayName("생성된 3개의 숫자는 서로 중복되지 않아야 한다") + void testNoDuplicateNumbers() { + ComputerPlayer player = new ComputerPlayer(); + + for (int i = 0; i < 100; i++) { + player.setRandomNumbers(); + int[] result = player.getNumbers(); + + Set numberSet = new HashSet<>(); + for (int num : result) { + numberSet.add(num); + } + + assertEquals(3, numberSet.size(), "생성된 숫자 3개 중 중복된 숫자가 존재합니다."); + } + } +} \ No newline at end of file diff --git a/src/test/java/view/InputViewTest.java b/src/test/java/view/InputViewTest.java new file mode 100644 index 00000000..97e98c0e --- /dev/null +++ b/src/test/java/view/InputViewTest.java @@ -0,0 +1,70 @@ +package view; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class InputViewTest { + + private InputView inputView; + private final InputStream standardIn = System.in; + + @AfterEach + void tearDown() { + System.setIn(standardIn); + } + + private void provideInput(String input) { + System.setIn(new ByteArrayInputStream(input.getBytes())); + inputView = new InputView(); + } + + @Test + @DisplayName("정상적인 3자리 숫자를 입력하면 int 배열을 반환한다") + void success_getUserNumbers() throws Exception { + provideInput("123"); + + int[] result = inputView.getUserNumbers(); + + assertArrayEquals(new int[]{1, 2, 3}, result); + } + + @ParameterizedTest + @ValueSource(strings = {"12", "1234", "abc", "102", "112", ""}) + @DisplayName("잘못된 형식의 숫자를 입력하면 예외가 발생한다") + void fail_getUserNumbers(String input) { + provideInput(input); + + assertThrows(IllegalArgumentException.class, () -> { + inputView.getUserNumbers(); + }); + } + + @Test + @DisplayName("재시작 옵션으로 1을 입력하면 정수 1을 반환한다") + void success_getResumeOption() throws Exception { + provideInput("1"); + + int result = inputView.getResumeOption(); + + assertEquals(1, result); + } + + @ParameterizedTest + @ValueSource(strings = {"3", "0", "a", "11"}) + @DisplayName("재시작 옵션으로 1 또는 2가 아닌 값을 입력하면 예외가 발생한다") + void fail_getResumeOption(String input) { + provideInput(input); + + assertThrows(IllegalArgumentException.class, () -> { + inputView.getResumeOption(); + }); + } +} \ No newline at end of file