diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..b529b3fb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Deun Lee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8d7e8aee..769cf601 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# java-baseball-precourse \ No newline at end of file +# Baseball-Game (Java) + +## 기능 요구사항 +- 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 맞추는 게임이다. + * 같은 수가 같은 자리에 있으면 `스트라이크` + * 다른 자리에 있으면 `볼` + * 같은 수가 전혀 없으면 `낫싱`이란 힌트를 얻음 +- 힌트를 이용해서 먼저 상대방의 수를 맞추면 승리한다. +- 상대방의 역할을 컴퓨터가 한다. + * 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택 + * 플레이어는 컴퓨터가 생각한 3개의 숫자를 입력, 컴퓨터는 입력한 숫자에 대한 결과를 출력 + * 이 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료됨 +- 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다. +- 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 메시지를 출력하고, 게임을 계속 진행할 수 있어야 한다. + +## 프로그래밍 요구사항 +- 자바 코드 컨벤션: https://naver.github.io/hackday-conventions-java/ +- indent depth가 3이 넘지 않도록 구현 (예를 들어 while문 안에 if문이 있으면 들여쓰기는 2임) +- 자바 8에 추가된 Stream API를 사용하지 않고 구현 (단, 람다는 사용 가능) +- else, switch/case 예약어를 쓰지 않음 (if 조건절에서 값을 return하는 방식으로 구현) +- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현 (한 가지 일만 잘 하도록 구현) +- 도메인 로직에 단위 테스트를 구현 (단, UI(System.out, System.in, Scanner) 로직은 제외) +- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현 +- MVC 패턴 기반으로 구현한 후 View, Controller를 제외한 Model에 대한 단위 테스트를 추가 +- JUnit5 기반 단위 테스트를 구현 + +## 과제 진행 요구사항 +- [next-step/java-baseball-precourse](https://github.com/next-step/java-baseball-precourse) 저장소를 fork/clone해 시작 +- 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가 +- commit 단위는 README.md 파일에 정리한 기능 목록 단위 또는 의미있는 단위 함 +- [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남기려고 노력 +- 과제 마감 시간: 2026.02.09 10시 + +## 프로그램 실행 결과 (예시): +``` +숫자를 입력해주세요 : 123 +1스트라이크 1볼 +숫자를 입력해주세요 : 145 +1볼 +숫자를 입력해주세요 : 671 +2볼 +숫자를 입력해주세요 : 216 +1스트라이크 +숫자를 입력해주세요 : 713 +3스트라이크 +3개의 숫자를 모두 맞히셨습니다! 게임 끝 +게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요. +1 +숫자를 입력해주세요 : 123 +1볼 +... +``` + +## 라이선스 +- MIT License diff --git "a/docs/\355\224\204\353\246\254\354\275\224\354\212\244_\354\210\253\354\236\220\354\225\274\352\265\254\352\262\214\354\236\204.pdf" "b/docs/\355\224\204\353\246\254\354\275\224\354\212\244_\354\210\253\354\236\220\354\225\274\352\265\254\352\262\214\354\236\204.pdf" new file mode 100644 index 00000000..b34414c6 Binary files /dev/null and "b/docs/\355\224\204\353\246\254\354\275\224\354\212\244_\354\210\253\354\236\220\354\225\274\352\265\254\352\262\214\354\236\204.pdf" differ diff --git "a/docs/\355\225\231\354\212\265\355\205\214\354\212\244\355\212\270\353\245\274 \355\206\265\355\225\264 JUnit \355\225\231\354\212\265\355\225\230\352\270\260.pdf" "b/docs/\355\225\231\354\212\265\355\205\214\354\212\244\355\212\270\353\245\274 \355\206\265\355\225\264 JUnit \355\225\231\354\212\265\355\225\230\352\270\260.pdf" new file mode 100644 index 00000000..48cc7b73 Binary files /dev/null and "b/docs/\355\225\231\354\212\265\355\205\214\354\212\244\355\212\270\353\245\274 \355\206\265\355\225\264 JUnit \355\225\231\354\212\265\355\225\230\352\270\260.pdf" differ diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/deunlee/baseball/Application.java b/src/main/java/com/deunlee/baseball/Application.java new file mode 100644 index 00000000..1e69f2c9 --- /dev/null +++ b/src/main/java/com/deunlee/baseball/Application.java @@ -0,0 +1,10 @@ +package com.deunlee.baseball; + +import com.deunlee.baseball.controller.GameController; + +public class Application { + public static void main(String[] args) { + GameController controller = new GameController(); + controller.run(); + } +} diff --git a/src/main/java/com/deunlee/baseball/controller/GameController.java b/src/main/java/com/deunlee/baseball/controller/GameController.java new file mode 100644 index 00000000..0a2f707b --- /dev/null +++ b/src/main/java/com/deunlee/baseball/controller/GameController.java @@ -0,0 +1,63 @@ +package com.deunlee.baseball.controller; + +import com.deunlee.baseball.model.BaseballGame; +import com.deunlee.baseball.model.BaseballNumberGenerator; +import com.deunlee.baseball.model.ComparisonResult; +import com.deunlee.baseball.model.Numbers; +import com.deunlee.baseball.view.TextInputView; +import com.deunlee.baseball.view.TextOutputView; + +public class GameController { + private final TextInputView inputView; + private final TextOutputView outputView; + + public GameController() { + this.inputView = new TextInputView(); + this.outputView = new TextOutputView(); + } + + public void run() { + outputView.printStartMessage(); + boolean isContinue = true; + while (isContinue) { + playGame(); + isContinue = shouldRestart(); + } + } + + private void playGame() { + final BaseballGame game = new BaseballGame(new BaseballNumberGenerator()); + ComparisonResult result = null; + while (result == null || !result.isWin()) { + result = processGuess(game); + } + outputView.printWinMessage(); + } + + private boolean shouldRestart() { + outputView.printRestartPrompt(); + try { + return inputView.readRestartDecision(); + } catch (IllegalArgumentException e) { + handleError(e); + return shouldRestart(); // 잘못된 값을 입력하면 다시 물어봄 + } + } + + private ComparisonResult processGuess(BaseballGame game) { + try { + outputView.printGuessPrompt(); + final Numbers playerNumbers = new Numbers(inputView.readPlayerGuess()); + final ComparisonResult result = game.checkResult(playerNumbers); + outputView.printResult(result); + return result; + } catch (IllegalArgumentException e) { + handleError(e); + return null; + } + } + + private void handleError(IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } +} diff --git a/src/main/java/com/deunlee/baseball/model/BaseballGame.java b/src/main/java/com/deunlee/baseball/model/BaseballGame.java new file mode 100644 index 00000000..50656282 --- /dev/null +++ b/src/main/java/com/deunlee/baseball/model/BaseballGame.java @@ -0,0 +1,38 @@ +package com.deunlee.baseball.model; + +public class BaseballGame { + private final Numbers answer; + + public BaseballGame(IRandomNumberGenerator generator) { + this.answer = new Numbers(generator.generateUniqueNumbers()); + } + + public ComparisonResult checkResult(final Numbers playerNumbers) { + int strikes = countStrikes(answer, playerNumbers); + int balls = countBalls(answer, playerNumbers); + return new ComparisonResult(strikes, balls); + } + + private int countStrikes(final Numbers answer, final Numbers guess) { + int strikes = 0; + for (int i = 0; i < 3; i++) { + if (answer.getDigitAt(i) == guess.getDigitAt(i)) { + strikes++; + } + } + return strikes; + } + + private int countBalls(final Numbers answer, final Numbers guess) { + int balls = 0; + for (int i = 0; i < 3; i++) { + if (answer.getDigitAt(i) == guess.getDigitAt(i)) { + continue; // 스트라이크는 제외 + } + if (answer.contains(guess.getDigitAt(i))) { + balls++; + } + } + return balls; + } +} diff --git a/src/main/java/com/deunlee/baseball/model/BaseballNumberGenerator.java b/src/main/java/com/deunlee/baseball/model/BaseballNumberGenerator.java new file mode 100644 index 00000000..873a3773 --- /dev/null +++ b/src/main/java/com/deunlee/baseball/model/BaseballNumberGenerator.java @@ -0,0 +1,23 @@ +package com.deunlee.baseball.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class BaseballNumberGenerator implements IRandomNumberGenerator { + private static final int REQUIRED_SIZE = 3; + + private final Random random = new Random(); + + @Override + public List generateUniqueNumbers() { + List numbers = new ArrayList<>(); + while (numbers.size() < REQUIRED_SIZE) { + int number = random.nextInt(9) + 1; // (1 ~ 9) + if (!numbers.contains(number)) { + numbers.add(number); + } + } + return numbers; + } +} diff --git a/src/main/java/com/deunlee/baseball/model/ComparisonResult.java b/src/main/java/com/deunlee/baseball/model/ComparisonResult.java new file mode 100644 index 00000000..39995f0a --- /dev/null +++ b/src/main/java/com/deunlee/baseball/model/ComparisonResult.java @@ -0,0 +1,21 @@ +package com.deunlee.baseball.model; + +public class ComparisonResult { + private static final int WIN_STRIKES = 3; + private static final int NOTHING = 0; + + private final int strikes; + private final int balls; + + public ComparisonResult(int strikes, int balls) { + this.strikes = strikes; + this.balls = balls; + } + + public int getStrikes() { return strikes; } + public int getBalls() { return balls; } + public boolean isWin() { return strikes == 3; } + public boolean isNothing() { return strikes == 0 && balls == 0; } + public boolean hasOnlyBalls() { return strikes == 0 && balls > 0; } + public boolean hasOnlyStrikes() { return balls == 0 && strikes > 0; } +} diff --git a/src/main/java/com/deunlee/baseball/model/IRandomNumberGenerator.java b/src/main/java/com/deunlee/baseball/model/IRandomNumberGenerator.java new file mode 100644 index 00000000..f19a1fdb --- /dev/null +++ b/src/main/java/com/deunlee/baseball/model/IRandomNumberGenerator.java @@ -0,0 +1,7 @@ +package com.deunlee.baseball.model; + +import java.util.List; + +public interface IRandomNumberGenerator { + List generateUniqueNumbers(); +} diff --git a/src/main/java/com/deunlee/baseball/model/Numbers.java b/src/main/java/com/deunlee/baseball/model/Numbers.java new file mode 100644 index 00000000..71bbeeb6 --- /dev/null +++ b/src/main/java/com/deunlee/baseball/model/Numbers.java @@ -0,0 +1,52 @@ +package com.deunlee.baseball.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class Numbers { + private static final int REQUIRED_SIZE = 3; + private static final int MIN_DIGIT = 1; + private static final int MAX_DIGIT = 9; + + private final List digits; + + public Numbers(List digits) { + validateSize(digits); + validateRange(digits); + validateUnique(digits); + this.digits = new ArrayList<>(digits); + } + + private void validateSize(final List digits) { + if (digits.size() != REQUIRED_SIZE) { + throw new IllegalArgumentException("숫자는 " + REQUIRED_SIZE + "개여야 합니다."); + } + } + + private void validateRange(final List digits) { + for (int digit : digits) { + if (digit < MIN_DIGIT || digit > MAX_DIGIT) { + throw new IllegalArgumentException("숫자는 " + MIN_DIGIT + "부터 " + MAX_DIGIT + " 사이여야 합니다."); + } + } + } + + private void validateUnique(final List digits) { + if (new HashSet<>(digits).size() != digits.size()) { + throw new IllegalArgumentException("숫자는 중복될 수 없습니다."); + } + } + + public int getDigitAt(int position) { + return digits.get(position); + } + + public boolean contains(int digit) { + return digits.contains(digit); + } + + public int size() { + return digits.size(); + } +} diff --git a/src/main/java/com/deunlee/baseball/view/TextInputView.java b/src/main/java/com/deunlee/baseball/view/TextInputView.java new file mode 100644 index 00000000..5ec52c6b --- /dev/null +++ b/src/main/java/com/deunlee/baseball/view/TextInputView.java @@ -0,0 +1,60 @@ +package com.deunlee.baseball.view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class TextInputView { + private static final int REQUIRED_LENGTH = 3; + private static final int RESTART_DECISION = 1; + + public List readPlayerGuess() { + final String input = readLine(); + validateLength(input, REQUIRED_LENGTH); + return convertToDigits(input); + } + + public boolean readRestartDecision() { + final String input = readLine(); + validateLength(input, 1); + int decision = convertToDigit(input.charAt(0)); + validateRestartDecision(decision); + return decision == RESTART_DECISION; + } + + private String readLine() { + Scanner scanner = new Scanner(System.in); + return scanner.nextLine(); + } + + private int convertToDigit(char c) { + validateDigit(c); + return c - '0'; + } + + private List convertToDigits(final String input) { + List digits = new ArrayList<>(); + for (char c : input.toCharArray()) { + digits.add(convertToDigit(c)); + } + return digits; + } + + private void validateLength(final String input, int length) { + if (input.length() != length) { + throw new IllegalArgumentException(length + "자리 숫자를 입력해야 합니다."); + } + } + + private void validateDigit(final char c) { + if (!Character.isDigit(c)) { + throw new IllegalArgumentException("숫자만 입력해야 합니다."); + } + } + + private void validateRestartDecision(int decision) { + if (decision != 1 && decision != 2) { + throw new IllegalArgumentException("1 또는 2를 입력해야 합니다."); + } + } +} diff --git a/src/main/java/com/deunlee/baseball/view/TextOutputView.java b/src/main/java/com/deunlee/baseball/view/TextOutputView.java new file mode 100644 index 00000000..20cecd6b --- /dev/null +++ b/src/main/java/com/deunlee/baseball/view/TextOutputView.java @@ -0,0 +1,38 @@ +package com.deunlee.baseball.view; + +import com.deunlee.baseball.model.ComparisonResult; + +public class TextOutputView { + + public void printStartMessage() { + System.out.println("숫자 야구 게임을 시작합니다."); + } + public void printGuessPrompt() { + System.out.print("숫자를 입력해주세요 : "); + } + public void printWinMessage() { + System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 끝"); + } + public void printRestartPrompt() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + } + public void printError(String message) { + System.out.println("[ERROR] " + message); + } + + public void printResult(final ComparisonResult result) { + if (result.isNothing()) { + System.out.println("낫싱"); + return; + } + if (result.hasOnlyBalls()) { + System.out.println(result.getBalls() + "볼"); + return; + } + if (result.hasOnlyStrikes()) { + System.out.println(result.getStrikes() + "스트라이크"); + return; + } + System.out.println(result.getBalls() + "볼 " + result.getStrikes() + "스트라이크"); + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/com/deunlee/baseball/model/BaseballGameTest.java b/src/test/java/com/deunlee/baseball/model/BaseballGameTest.java new file mode 100644 index 00000000..7e75d4eb --- /dev/null +++ b/src/test/java/com/deunlee/baseball/model/BaseballGameTest.java @@ -0,0 +1,100 @@ +package com.deunlee.baseball.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BaseballGameTest { + + @DisplayName("3스트라이크: 모든 숫자와 위치가 일치") + @Test + void testThreeStrikes() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(1, 2, 3)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(3); + assertThat(result.getBalls()).isEqualTo(0); + assertThat(result.isWin()).isTrue(); + } + + @DisplayName("3볼: 모든 숫자는 맞지만 위치가 모두 다름") + @Test + void testThreeBalls() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(3, 1, 2)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(0); + assertThat(result.getBalls()).isEqualTo(3); + assertThat(result.isWin()).isFalse(); + } + + @DisplayName("1스트라이크 1볼") + @Test + void testOneStrikeOneBall() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(1, 3, 4)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(1); + assertThat(result.getBalls()).isEqualTo(1); + assertThat(result.isWin()).isFalse(); + } + + @DisplayName("2스트라이크 0볼") + @Test + void testTwoStrikes() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(1, 2, 4)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(2); + assertThat(result.getBalls()).isEqualTo(0); + assertThat(result.isWin()).isFalse(); + } + + @DisplayName("낫싱: 일치하는 숫자가 없음") + @Test + void testNothing() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(4, 5, 6)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(0); + assertThat(result.getBalls()).isEqualTo(0); + assertThat(result.isNothing()).isTrue(); + } + + @DisplayName("1스트라이크 2볼") + @Test + void testOneStrikeTwoBalls() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(1, 3, 2)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(1); + assertThat(result.getBalls()).isEqualTo(2); + } + + @DisplayName("0스트라이크 2볼") + @Test + void testTwoBalls() { + IRandomNumberGenerator generator = () -> List.of(1, 2, 3); + BaseballGame game = new BaseballGame(generator); + Numbers playerNumbers = new Numbers(List.of(2, 1, 4)); + ComparisonResult result = game.checkResult(playerNumbers); + + assertThat(result.getStrikes()).isEqualTo(0); + assertThat(result.getBalls()).isEqualTo(2); + } +} diff --git a/src/test/java/com/deunlee/baseball/model/ComparisonResultTest.java b/src/test/java/com/deunlee/baseball/model/ComparisonResultTest.java new file mode 100644 index 00000000..291907aa --- /dev/null +++ b/src/test/java/com/deunlee/baseball/model/ComparisonResultTest.java @@ -0,0 +1,63 @@ +package com.deunlee.baseball.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComparisonResultTest { + + @Test + @DisplayName("3스트라이크는 승리") + void testWin() { + ComparisonResult result = new ComparisonResult(3, 0); + + assertThat(result.isWin()).isTrue(); + assertThat(result.getStrikes()).isEqualTo(3); + assertThat(result.getBalls()).isEqualTo(0); + } + + @Test + @DisplayName("볼만 있는 경우") + void testOnlyBalls() { + ComparisonResult result = new ComparisonResult(0, 2); + + assertThat(result.isWin()).isFalse(); + assertThat(result.isNothing()).isFalse(); + assertThat(result.getStrikes()).isEqualTo(0); + assertThat(result.getBalls()).isEqualTo(2); + } + + @Test + @DisplayName("스트라이크 하나만 있는 경우") + void testOnlyStrikes() { + ComparisonResult result = new ComparisonResult(1, 0); + + assertThat(result.isWin()).isFalse(); + assertThat(result.isNothing()).isFalse(); + assertThat(result.getStrikes()).isEqualTo(1); + assertThat(result.getBalls()).isEqualTo(0); + } + + @Test + @DisplayName("스트라이크와 볼 혼합") + void testMixed() { + ComparisonResult result = new ComparisonResult(1, 2); + + assertThat(result.isWin()).isFalse(); + assertThat(result.isNothing()).isFalse(); + assertThat(result.getStrikes()).isEqualTo(1); + assertThat(result.getBalls()).isEqualTo(2); + } + + @Test + @DisplayName("낫싱 (0스트라이크 0볼)") + void testNothing() { + ComparisonResult result = new ComparisonResult(0, 0); + + assertThat(result.isNothing()).isTrue(); + assertThat(result.isWin()).isFalse(); + assertThat(result.getStrikes()).isEqualTo(0); + assertThat(result.getBalls()).isEqualTo(0); + } +} diff --git a/src/test/java/com/deunlee/baseball/model/NumbersTest.java b/src/test/java/com/deunlee/baseball/model/NumbersTest.java new file mode 100644 index 00000000..37648549 --- /dev/null +++ b/src/test/java/com/deunlee/baseball/model/NumbersTest.java @@ -0,0 +1,82 @@ +package com.deunlee.baseball.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class NumbersTest { + + @DisplayName("3개의 서로 다른 1~9 숫자로 생성") + @Test + void createValidNumbers() { + Numbers numbers = new Numbers(List.of(1, 2, 3)); + + assertThat(numbers.size()).isEqualTo(3); + assertThat(numbers.getDigitAt(0)).isEqualTo(1); + assertThat(numbers.getDigitAt(1)).isEqualTo(2); + assertThat(numbers.getDigitAt(2)).isEqualTo(3); + } + + @DisplayName("contains 메서드로 숫자 포함 여부 확인") + @Test + void testContains() { + Numbers numbers = new Numbers(List.of(1, 2, 3)); + + assertThat(numbers.contains(1)).isTrue(); + assertThat(numbers.contains(2)).isTrue(); + assertThat(numbers.contains(3)).isTrue(); + assertThat(numbers.contains(4)).isFalse(); + } + + @DisplayName("2개의 숫자는 예외 발생") + @Test + void throwExceptionWhenSizeIsTwo() { + assertThatThrownBy(() -> new Numbers(List.of(1, 2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 3개여야 합니다."); + } + + @DisplayName("4개의 숫자는 예외 발생") + @Test + void throwExceptionWhenSizeIsFour() { + assertThatThrownBy(() -> new Numbers(List.of(1, 2, 3, 4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 3개여야 합니다."); + } + + @DisplayName("중복된 숫자는 예외 발생") + @Test + void throwExceptionWhenDuplicate() { + assertThatThrownBy(() -> new Numbers(List.of(1, 1, 2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 중복될 수 없습니다."); + } + + @DisplayName("0은 예외 발생") + @Test + void throwExceptionWhenZero() { + assertThatThrownBy(() -> new Numbers(List.of(0, 1, 2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 1부터 9 사이여야 합니다."); + } + + @DisplayName("10 이상은 예외 발생") + @Test + void throwExceptionWhenTenOrMore() { + assertThatThrownBy(() -> new Numbers(List.of(1, 2, 10))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 1부터 9 사이여야 합니다."); + } + + @DisplayName("음수는 예외 발생") + @Test + void throwExceptionWhenNegative() { + assertThatThrownBy(() -> new Numbers(List.of(-1, 2, 3))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("숫자는 1부터 9 사이여야 합니다."); + } +}