diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e72c072d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# top-most EditorConfig file +root = true + +[*] +# [encoding-utf8] +charset = utf-8 + +# [newline-lf] +end_of_line = lf + +# [newline-eof] +insert_final_newline = true + +[*.bat] +end_of_line = crlf + +[*.java] +# [indentation-tab] +indent_style = tab + +# [4-spaces-tab] +indent_size = 4 +tab_width = 4 + +# [no-trailing-spaces] +trim_trailing_whitespace = true + +[line-length-120] +max_line_length = 120 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9423ff36 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +*.c text eol=lf +*.cpp text eol=lf +*.h text eol=lf + +# exception for visual studio project configuration +*.sln text eol=crlf +*.vs text eol=crlf +*.csproj eol=crlf +*.props eol=crlf +*.filters eol=crlf diff --git a/README.md b/README.md index 8d7e8aee..fbe9496e 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +> 컴퓨터가 제시하는 임의의 숫자 3개를 모두 맞히는게 목표인 게임 +> +> 맞힐 때마다 라운드가 진행되며, 라운드 제한은 없음 +> +> 라운드 진행 후, 각 세부 규칙마다 볼, 스트라이크를 출력 +> 스트라이크 = 같은 수 + 같은 자리 +> +> 볼 = 같은 수 + 다른 자리 + +## 핵심 로직 + +- 정답 생성: 1에서 9까지 서로 다른 임의의 수 3개 생성. + +- 숫자 비교: 사용자가 입력한 수와 컴퓨터의 수를 비교. + +- 같은 수 + 같은 자리 = 스트라이크 + +- 같은 수 + 다른 자리 = 볼 + +- 일치하는 숫자가 전혀 없음 = 낫싱 + +- 결과 출력: 계산된 힌트(스트라이크, 볼, 낫싱)를 형식에 맞춰 출력. + +- 승리 조건: 3개의 숫자를 모두 맞히면 게임 종료 및 재시작/종료 선택창 출력. + +### 인터페이스 제약 사항 + +**입력** + +- 서로 다른 3자리의 수 (각 자리는 1~9) +- 게임 재시작/종료 선택 시 1(재시작) 또는 2(종료) +- 잘못된 값 입력 시 `IllegalArgumentException` 발생 + +**출력** + +- 입력한 수에 대한 결과를 볼, 스트라이크 개수로 표시 +- 하나도 없는 경우 "낫싱" +- 3개의 숫자를 모두 맞힐 경우 게임 종료 문구 출력 + +## 커밋 계획 + +### 사전 구성 + +1. style: 코드 포맷팅 구성 ✅ +2. docs: 기능 요구 사항 정리 및 구현 계획 작성 ✅ +3. chore: junit 학습 테스트 실행 및 적용 ✅ + +### 핵심 로직 (Domain) + +4. feat: 1~9 서로 다른 임의의 수 3개 생성 구현 (Green) ✅ +5. test: 볼, 스트라이크 비교 로직 테스트 ✅ +6. feat: 볼, 스트라이크 비교 및 판별 로직 구현 (Green) ✅ +7. refactor: 비교 로직 가독성 개선 (Refactor) ✅ + +### 게임 진행 (Controller) + +8. feat: 게임 흐름 제어 구현 ✅ +9. feat: 게임 메인 플레이 로직 및 도메인 생성 ✅ +11. refactor: 전체 구조 리팩토링 및 뷰 컨트롤러 - 뷰 간 메소드 분리 (Refactor) + +### 입출력 및 유효성 검사 (View/Validation) + +12. test: 유저 숫자 입력 유효성 검사 (3자리 숫자, 중복 확인) ✅ +13. feat: 유저 입력 파싱 및 예외 처리 구현 (Green) ✅ +14. test: 게임 재시작/종료 입력 검증 ✅ +15. feat: 재시작(1)/종료(2) 입력 처리 구현 (Green) ✅ + diff --git a/build.gradle b/build.gradle index 20a92c9e..0df928b1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,9 @@ plugins { id 'java' } - +// 방법1 +compileJava.options.encoding = 'UTF-8' +compileTestJava.options.encoding = 'UTF-8' group = 'camp.nextstep.edu' version = '1.0-SNAPSHOT' @@ -18,8 +20,10 @@ repositories { dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.assertj:assertj-core:3.25.3' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.0' } + test { useJUnitPlatform() } diff --git a/src/main/java/baseball/BaseBall.java b/src/main/java/baseball/BaseBall.java new file mode 100644 index 00000000..d78353e8 --- /dev/null +++ b/src/main/java/baseball/BaseBall.java @@ -0,0 +1,12 @@ +package baseball; + +import baseball.coordinator.MainCoordinator; + +public class BaseBall { + + public static void main(String[] args) { + MainCoordinator mainCoordinator = new MainCoordinator(); + mainCoordinator.start(); + } +} + diff --git a/src/main/java/baseball/DIContainer.java b/src/main/java/baseball/DIContainer.java new file mode 100644 index 00000000..bc201b49 --- /dev/null +++ b/src/main/java/baseball/DIContainer.java @@ -0,0 +1,45 @@ +package baseball; + +import java.util.List; + +import baseball.domain.number_generator.INumberGenerator; +import baseball.domain.number_generator.IntNumberGenerator; +import baseball.domain.score_calculator.BallCalculator; +import baseball.domain.score_calculator.IScoreCalculator; +import baseball.domain.score_calculator.StrikeCalculator; +import baseball.domain.strike_zone_calculator.IStrikeZoneCalculator; +import baseball.domain.strike_zone_calculator.StrikeZoneCalculator; + +public class DIContainer { + public static DIContainer shared = new DIContainer(); + + public final Domain domain; + public final Util util; + + private DIContainer() { + this.util = new Util(); + this.domain = new Domain(util); + } + + public static class Util { + final public int generateNumber = 3; + + Util() { + + } + } + + public static class Domain { + final public INumberGenerator> integerNumberGenerator; + final public IStrikeZoneCalculator strikeZoneCalculator; + final public IScoreCalculator strikeCalculator; + final public IScoreCalculator ballCalculator; + + Domain(Util util) { + integerNumberGenerator = new IntNumberGenerator(); + strikeCalculator = new StrikeCalculator(); + ballCalculator = new BallCalculator(strikeCalculator); + strikeZoneCalculator = new StrikeZoneCalculator(this.strikeCalculator, this.ballCalculator); + } + } +} diff --git a/src/main/java/baseball/coordinator/Coordinator.java b/src/main/java/baseball/coordinator/Coordinator.java new file mode 100644 index 00000000..7cfd502b --- /dev/null +++ b/src/main/java/baseball/coordinator/Coordinator.java @@ -0,0 +1,9 @@ +package baseball.coordinator; + +import java.util.List; + +public interface Coordinator { + List childCoordinators = List.of(); + + void start(); +} diff --git a/src/main/java/baseball/coordinator/MainCoordinator.java b/src/main/java/baseball/coordinator/MainCoordinator.java new file mode 100644 index 00000000..6cdc4fdf --- /dev/null +++ b/src/main/java/baseball/coordinator/MainCoordinator.java @@ -0,0 +1,54 @@ +package baseball.coordinator; + +import baseball.DIContainer; +import baseball.view_controller.home_view_controller.HomeViewController; +import baseball.view_controller.home_view_controller.HomeViewControllerDelegate; +import baseball.view_controller.play_view_controller.PlayViewController; +import baseball.view_controller.play_view_controller.PlayViewControllerDelegate; + +public class MainCoordinator implements Coordinator, PlayViewControllerDelegate, HomeViewControllerDelegate { + private final DIContainer diContainer = DIContainer.shared; + private HomeViewController homeViewController; + private PlayViewController playViewController; + + // --- Coordinator 구현 --- + @Override + public void start() { + showPlay(); + } + + // --- HomeViewControllerDelegate 구현 --- + @Override + public void userPlaySelected() { + showPlay(); + } + + @Override + public void userExitSelected() { + homeViewController = null; + playViewController = null; + } + + // --- PlayViewControllerDelegate 구현 --- + @Override + public void playFinished() { + showHome(); + } + + private void showHome() { + this.homeViewController = new HomeViewController(); + this.homeViewController.delegate = this; + this.homeViewController.render(); + } + + private void showPlay() { + // PlayViewController에 필요한 의존성을 여기서 생성하고 주입합니다. + this.playViewController = new PlayViewController( + diContainer.util.generateNumber, + diContainer.domain.integerNumberGenerator, + diContainer.domain.strikeZoneCalculator + ); + this.playViewController.delegate = this; + this.playViewController.render(); + } +} diff --git a/src/main/java/baseball/domain/entity/BallCountEntity.java b/src/main/java/baseball/domain/entity/BallCountEntity.java new file mode 100644 index 00000000..70e6a8ce --- /dev/null +++ b/src/main/java/baseball/domain/entity/BallCountEntity.java @@ -0,0 +1,34 @@ +package baseball.domain.entity; + +import java.util.Objects; + +public class BallCountEntity { + public int ball; + public int strike; + + public BallCountEntity(int strikeCount, int ballCount) { + this.strike = strikeCount; + this.ball = ballCount; + } + + @Override + public boolean equals(Object o) { + // 1. 자기 자신과 비교하면 true + if (this == o) + return true; + + // 2. null이거나 클래스 종류가 다르면 false + if (o == null || getClass() != o.getClass()) + return false; + + // 3. 값을 비교 (strike와 ball이 모두 같아야 true) + BallCountEntity that = (BallCountEntity)o; + return ball == that.ball && strike == that.strike; + } + + @Override + public int hashCode() { + // 객체의 값을 기반으로 고유 번호 생성 (HashMap 등에서 필수) + return Objects.hash(ball, strike); + } +} diff --git a/src/main/java/baseball/domain/entity/StrikeZoneResult.java b/src/main/java/baseball/domain/entity/StrikeZoneResult.java new file mode 100644 index 00000000..a119d446 --- /dev/null +++ b/src/main/java/baseball/domain/entity/StrikeZoneResult.java @@ -0,0 +1,67 @@ +package baseball.domain.entity; + +import java.util.Objects; + +public class StrikeZoneResult { + private final Type type; + private final BallCountEntity ballCount; + + // 생성자들을 private으로 막고 Factory 메서드 사용 + private StrikeZoneResult(Type type, BallCountEntity ballCount) { + this.type = type; + this.ballCount = ballCount; + } + + // Factory 메서드들 (Swift의 case 생성자 역할) + public static StrikeZoneResult nothing() { + return new StrikeZoneResult(Type.NOTHING, new BallCountEntity(0, 0)); + } + + public static StrikeZoneResult playing(BallCountEntity ballCount) { + return new StrikeZoneResult(Type.PLAYING, ballCount); + } + + public static StrikeZoneResult complete(BallCountEntity ballCount) { + return new StrikeZoneResult(Type.COMPLETE, ballCount); + } + + // Getter + public Type getType() { + return type; + } + + public BallCountEntity get() { + return this.ballCount; + } + + public int getBall() { + return this.ballCount.ball; + } + + public int getStrike() { + return this.ballCount.strike; + } + + @Override + public boolean equals(Object o) { + // 1. 주소값이 같으면 같은 객체 + if (this == o) + return true; + // 2. null이거나 클래스 타입이 다르면 다른 객체 + if (o == null || getClass() != o.getClass()) + return false; + + // 3. 타입 캐스팅 후 필드값 비교 + StrikeZoneResult that = (StrikeZoneResult)o; + return type == that.type && Objects.equals(ballCount, that.ballCount); + } + + @Override + public int hashCode() { + // equals에 사용된 필드들로 해시코드 생성 + return Objects.hash(type, ballCount); + } + + // 내부 Enum 정의 (종류 구분용) + public enum Type {NOTHING, PLAYING, COMPLETE} +} diff --git a/src/main/java/baseball/domain/entity/UserPlayMode.java b/src/main/java/baseball/domain/entity/UserPlayMode.java new file mode 100644 index 00000000..567cb95e --- /dev/null +++ b/src/main/java/baseball/domain/entity/UserPlayMode.java @@ -0,0 +1,3 @@ +package baseball.domain.entity; + +public enum UserPlayMode {Start, Finish} diff --git a/src/main/java/baseball/domain/number_generator/INumberGenerator.java b/src/main/java/baseball/domain/number_generator/INumberGenerator.java new file mode 100644 index 00000000..67b25ef9 --- /dev/null +++ b/src/main/java/baseball/domain/number_generator/INumberGenerator.java @@ -0,0 +1,7 @@ +package baseball.domain.number_generator; + +import java.util.Date; + +public interface INumberGenerator { + T execute(Date date, Integer generateNumberCount); +} diff --git a/src/main/java/baseball/domain/number_generator/IntNumberGenerator.java b/src/main/java/baseball/domain/number_generator/IntNumberGenerator.java new file mode 100644 index 00000000..79353836 --- /dev/null +++ b/src/main/java/baseball/domain/number_generator/IntNumberGenerator.java @@ -0,0 +1,22 @@ +package baseball.domain.number_generator; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +public class IntNumberGenerator implements INumberGenerator> { + + @Override + public List execute(Date date, Integer generateNumberCount) { + Random random = new Random(date.getTime()); + List numbers = new ArrayList<>(); + while (numbers.size() < generateNumberCount) { + int number = random.nextInt(9) + 1; + if (numbers.contains(number)) + continue; + numbers.add(number); + } + return numbers; + } +} diff --git a/src/main/java/baseball/domain/score_calculator/BallCalculator.java b/src/main/java/baseball/domain/score_calculator/BallCalculator.java new file mode 100644 index 00000000..94238030 --- /dev/null +++ b/src/main/java/baseball/domain/score_calculator/BallCalculator.java @@ -0,0 +1,26 @@ +package baseball.domain.score_calculator; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class BallCalculator implements IScoreCalculator { + + private final IScoreCalculator strikeCalculator; + + public BallCalculator(IScoreCalculator strikeCalculator) { + this.strikeCalculator = strikeCalculator; + } + + @Override + public int calculate(List human, List computer) { + if (human.size() != computer.size()) { + throw new IllegalArgumentException("두 수의 갯수가 다릅니다!"); + } + Set humanSet = new HashSet<>(human); + Set computerSet = new HashSet<>(computer); + + humanSet.retainAll(computerSet); + return Integer.max(0, humanSet.size() - strikeCalculator.calculate(human, computer)); + } +} diff --git a/src/main/java/baseball/domain/score_calculator/IScoreCalculator.java b/src/main/java/baseball/domain/score_calculator/IScoreCalculator.java new file mode 100644 index 00000000..1e7a9b1c --- /dev/null +++ b/src/main/java/baseball/domain/score_calculator/IScoreCalculator.java @@ -0,0 +1,7 @@ +package baseball.domain.score_calculator; + +import java.util.List; + +public interface IScoreCalculator { + int calculate(List human, List computer); +} diff --git a/src/main/java/baseball/domain/score_calculator/StrikeCalculator.java b/src/main/java/baseball/domain/score_calculator/StrikeCalculator.java new file mode 100644 index 00000000..1424c333 --- /dev/null +++ b/src/main/java/baseball/domain/score_calculator/StrikeCalculator.java @@ -0,0 +1,19 @@ +package baseball.domain.score_calculator; + +import java.util.List; + +public class StrikeCalculator implements IScoreCalculator { + @Override + public int calculate(List human, List computer) { + if (human.size() != computer.size()) { + throw new IllegalArgumentException("두 수의 갯수가 다릅니다!"); + } + int count = 0; + for (int i = 0; i < human.size(); i++) { + if (human.get(i).equals(computer.get(i))) { + count++; + } + } + return count; + } +} diff --git a/src/main/java/baseball/domain/strike_zone_calculator/IStrikeZoneCalculator.java b/src/main/java/baseball/domain/strike_zone_calculator/IStrikeZoneCalculator.java new file mode 100644 index 00000000..2dd457dd --- /dev/null +++ b/src/main/java/baseball/domain/strike_zone_calculator/IStrikeZoneCalculator.java @@ -0,0 +1,13 @@ +package baseball.domain.strike_zone_calculator; + +import java.util.List; + +import baseball.domain.entity.StrikeZoneResult; +import baseball.domain.score_calculator.IScoreCalculator; + +public interface IStrikeZoneCalculator { + IScoreCalculator strikeCalculator = null; + IScoreCalculator ballCalculator = null; + + StrikeZoneResult execute(List human, List computer); +} diff --git a/src/main/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculator.java b/src/main/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculator.java new file mode 100644 index 00000000..48362d96 --- /dev/null +++ b/src/main/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculator.java @@ -0,0 +1,38 @@ +package baseball.domain.strike_zone_calculator; + +import java.util.List; + +import baseball.domain.entity.BallCountEntity; +import baseball.domain.entity.StrikeZoneResult; +import baseball.domain.score_calculator.IScoreCalculator; + +/// Strike Zone Ball Count를 연산하는 객체 +public class StrikeZoneCalculator implements IStrikeZoneCalculator { + private final IScoreCalculator strikeCalculator; + private final IScoreCalculator ballCalculator; + + public StrikeZoneCalculator( + IScoreCalculator strikeCalculator, + IScoreCalculator ballCalculator + ) { + this.strikeCalculator = strikeCalculator; + this.ballCalculator = ballCalculator; + } + + @Override + public StrikeZoneResult execute(List human, List computer) { + if (human.size() != computer.size()) { + throw new IllegalStateException("각 답의 개수가 맞지 않습니다!"); + } + + int strikeCount = strikeCalculator.calculate(human, computer); + int ballCount = ballCalculator.calculate(human, computer); + BallCountEntity entity = new BallCountEntity(strikeCount, ballCount); + + if (strikeCount == computer.size()) + return StrikeZoneResult.complete(entity); + if (entity.strike == 0 && entity.ball == 0) + return StrikeZoneResult.nothing(); + return StrikeZoneResult.playing(entity); + } +} diff --git a/src/main/java/baseball/view/HomeView.java b/src/main/java/baseball/view/HomeView.java new file mode 100644 index 00000000..0904ef3b --- /dev/null +++ b/src/main/java/baseball/view/HomeView.java @@ -0,0 +1,38 @@ +package baseball.view; + +import java.util.Scanner; + +import baseball.domain.entity.UserPlayMode; + +public class HomeView { + private final Scanner scanner = new Scanner(System.in); + + public void askForGameOption() { + System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."); + } + + public UserPlayMode readUserPlayGame() { + String input = scanner.nextLine().trim(); // 앞뒤 공백 제거 및 줄 단위 읽기 + try { + int userMode = Integer.parseInt(input); + + if (userMode == 1) { + return UserPlayMode.Start; + } + if (userMode == 2) { + return UserPlayMode.Finish; + } + throw new IllegalArgumentException("1 또는 2만 입력 가능합니다.."); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("숫자가 아닌 잘못된 형식을 입력하셨습니다."); + } + } + + public void showErrorMessage(String message) { + System.out.println("[ERROR] " + message); + } + + public void showUserEndGameMessage() { + System.out.println("게임을 종료하였습니다 바이 바이!!"); + } +} diff --git a/src/main/java/baseball/view/PlayView.java b/src/main/java/baseball/view/PlayView.java new file mode 100644 index 00000000..3e01e746 --- /dev/null +++ b/src/main/java/baseball/view/PlayView.java @@ -0,0 +1,62 @@ +package baseball.view; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import baseball.domain.entity.StrikeZoneResult; + +public class PlayView { + private final Scanner scanner = new Scanner(System.in); + + public List readPlayerNumber() { + System.out.print("숫자를 입력해주세요: "); + String input = scanner.next(); + + List numbers = new ArrayList<>(); + + for (char c : input.toCharArray()) { + if (!Character.isDigit(c)) { + throw new IllegalArgumentException("숫자만 입력해야 합니다."); + } + int value = Character.getNumericValue(c); + if (numbers.contains(value)) { + throw new IllegalArgumentException("중복된 숫자가 있습니다."); + } + numbers.add(value); + } + return numbers; + } + + public void showErrorMessage(String message) { + System.out.println("[ERROR] " + message); + } + + public void showUserMatchMessage(StrikeZoneResult result) { + System.out.print(userMatchRouter(result)); + } + + public void showGameCompletedMessage() { + System.out.println("게임 끝!"); + } + + private String userMatchRouter(StrikeZoneResult result) { + StrikeZoneResult.Type resultType = result.getType(); + if (resultType == StrikeZoneResult.Type.COMPLETE) { + return result.getStrike() + "개의 숫자를 모두 맞히셨습니다! "; + } + if (resultType == StrikeZoneResult.Type.NOTHING) { + return "낫싱\n"; + } + int ballCnt = result.getBall(); + int strikeCnt = result.getStrike(); + List resList = new ArrayList<>(); + + if (strikeCnt > 0) + resList.add(strikeCnt + "스트라이크"); + if (ballCnt > 0) + resList.add(ballCnt + "볼"); + return String.join(" ", resList) + "\n"; + } +} + diff --git a/src/main/java/baseball/view_controller/common/ViewController.java b/src/main/java/baseball/view_controller/common/ViewController.java new file mode 100644 index 00000000..a3127646 --- /dev/null +++ b/src/main/java/baseball/view_controller/common/ViewController.java @@ -0,0 +1,5 @@ +package baseball.view_controller.common; + +public interface ViewController { + void render(); +} diff --git a/src/main/java/baseball/view_controller/home_view_controller/HomeViewController.java b/src/main/java/baseball/view_controller/home_view_controller/HomeViewController.java new file mode 100644 index 00000000..b4982c4c --- /dev/null +++ b/src/main/java/baseball/view_controller/home_view_controller/HomeViewController.java @@ -0,0 +1,31 @@ +package baseball.view_controller.home_view_controller; + +import baseball.domain.entity.UserPlayMode; +import baseball.view.HomeView; +import baseball.view_controller.common.ViewController; + +public class HomeViewController implements ViewController { + private final HomeView homeView = new HomeView(); + public HomeViewControllerDelegate delegate; + + @Override + public void render() { + try { + UserPlayMode mode = homeView.readUserPlayGame(); + if (mode == UserPlayMode.Start) { + delegate.userPlaySelected(); + return; + } + + if (mode == UserPlayMode.Finish) { + homeView.showUserEndGameMessage(); + delegate.userExitSelected(); + } + } catch (IllegalArgumentException e) { + homeView.showErrorMessage(e.getMessage()); + render(); + } + } +} + + diff --git a/src/main/java/baseball/view_controller/home_view_controller/HomeViewControllerDelegate.java b/src/main/java/baseball/view_controller/home_view_controller/HomeViewControllerDelegate.java new file mode 100644 index 00000000..3636fcb4 --- /dev/null +++ b/src/main/java/baseball/view_controller/home_view_controller/HomeViewControllerDelegate.java @@ -0,0 +1,7 @@ +package baseball.view_controller.home_view_controller; + +public interface HomeViewControllerDelegate { + void userExitSelected(); + + void userPlaySelected(); +} diff --git a/src/main/java/baseball/view_controller/play_view_controller/PlayViewController.java b/src/main/java/baseball/view_controller/play_view_controller/PlayViewController.java new file mode 100644 index 00000000..a1d69b15 --- /dev/null +++ b/src/main/java/baseball/view_controller/play_view_controller/PlayViewController.java @@ -0,0 +1,50 @@ +package baseball.view_controller.play_view_controller; + +import java.util.Date; +import java.util.List; + +import baseball.domain.entity.StrikeZoneResult; +import baseball.domain.number_generator.INumberGenerator; +import baseball.domain.strike_zone_calculator.IStrikeZoneCalculator; +import baseball.view.PlayView; +import baseball.view_controller.common.ViewController; + +public class PlayViewController implements ViewController { + private final INumberGenerator> numberGenerator; + private final IStrikeZoneCalculator strikeZoneCalculator; + private final PlayView playView = new PlayView(); + private final int generateNumber; + public PlayViewControllerDelegate delegate; + + public PlayViewController( + int generateNumber, + INumberGenerator> numberGenerator, + IStrikeZoneCalculator strikeZoneCalculator + ) { + this.generateNumber = generateNumber; + this.numberGenerator = numberGenerator; + this.strikeZoneCalculator = strikeZoneCalculator; + } + + @Override + public void render() { + List computerNumber = numberGenerator.execute(new Date(), generateNumber); + while (true) { + try { + List userNumber = playView.readPlayerNumber(); + if (userNumber.size() != generateNumber) { + throw new IllegalArgumentException("유저 입력 형식이 맞지 않습니다!!"); + } + StrikeZoneResult result = strikeZoneCalculator.execute(userNumber, computerNumber); + playView.showUserMatchMessage(result); + if (result.getType() == StrikeZoneResult.Type.COMPLETE) { + playView.showGameCompletedMessage(); + break; + } + } catch (IllegalArgumentException e) { // ( ) 괄호 추가! + playView.showErrorMessage(e.getMessage()); + } + } + delegate.playFinished(); + } +} diff --git a/src/main/java/baseball/view_controller/play_view_controller/PlayViewControllerDelegate.java b/src/main/java/baseball/view_controller/play_view_controller/PlayViewControllerDelegate.java new file mode 100644 index 00000000..89d2fe2a --- /dev/null +++ b/src/main/java/baseball/view_controller/play_view_controller/PlayViewControllerDelegate.java @@ -0,0 +1,5 @@ +package baseball.view_controller.play_view_controller; + +public interface PlayViewControllerDelegate { + void playFinished(); +} diff --git a/src/test/java/baseball/domain/number_generator/NumberGeneratorTest.java b/src/test/java/baseball/domain/number_generator/NumberGeneratorTest.java new file mode 100644 index 00000000..9fa788b1 --- /dev/null +++ b/src/test/java/baseball/domain/number_generator/NumberGeneratorTest.java @@ -0,0 +1,42 @@ +package baseball.domain.number_generator; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class NumberGeneratorTest { + static int generateNumberCount = 3; + private final INumberGenerator> numberGenerator = new IntNumberGenerator(); + + @DisplayName("1에서 9까지 서로 다른 임의의 수 3개를 생성한다") + @Test + void generateIntNumbers() { + // given + Date now = new Date(); + + // when + List generatedNumbers = numberGenerator.execute(now, 3); + + // then + assertThat(generatedNumbers).hasSize(3); + assertThat(generatedNumbers).allMatch(number -> number >= 1 && number <= 9); + } + + @DisplayName("고정된 날짜(시드)를 입력하면 항상 동일한 숫자 목록을 반환한다") + @Test + void generateFixedNumbers() { + // given + Date fixedDate = new Date(1000L); // 고정된 타임스탬프 사용 + + // when + List result1 = numberGenerator.execute(fixedDate, generateNumberCount); + List result2 = numberGenerator.execute(fixedDate, generateNumberCount); + + // then + assertThat(result1).isEqualTo(result2); + } +} diff --git a/src/test/java/baseball/domain/score_calculator/ScoreCalculatorTest.java b/src/test/java/baseball/domain/score_calculator/ScoreCalculatorTest.java new file mode 100644 index 00000000..93a09302 --- /dev/null +++ b/src/test/java/baseball/domain/score_calculator/ScoreCalculatorTest.java @@ -0,0 +1,57 @@ +package baseball.domain.score_calculator; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class ScoreCalculatorTest { + IScoreCalculator strikeCalculator; + IScoreCalculator ballCalculator; + + static List strikeTestCases() { + return List.of( + Arguments.of(List.of(1, 2, 3), List.of(1, 2, 3), 3), + Arguments.of(List.of(3, 2, 1), List.of(1, 2, 3), 1), + Arguments.of(List.of(3, 1, 2), List.of(1, 2, 3), 0), + Arguments.of(List.of(1, 5, 4), List.of(1, 2, 3), 1) + ); + } + + static List ballTestCases() { + return List.of( + Arguments.of(List.of(1, 2, 3), List.of(1, 2, 3), 0), + Arguments.of(List.of(3, 2, 1), List.of(1, 2, 3), 2), + Arguments.of(List.of(3, 1, 2), List.of(1, 2, 3), 3), + Arguments.of(List.of(1, 5, 4), List.of(1, 2, 3), 0) + ); + } + + @BeforeEach + void setUp() { + this.strikeCalculator = new StrikeCalculator(); + this.ballCalculator = new BallCalculator(this.strikeCalculator); + } + + @DisplayName("볼 계산기 테스트") + @ParameterizedTest + @MethodSource("ballTestCases") + void ballCalculator(List human, List computer, int expected) { + int result = ballCalculator.calculate(human, computer); + assertThat(result).isEqualTo(expected); + } + + @DisplayName("스트라이크 계산기 테스트") + @ParameterizedTest + @MethodSource("strikeTestCases") + void strikeCalculator(List human, List computer, int expected) { + + int result = strikeCalculator.calculate(human, computer); + assertThat(result).isEqualTo(expected); + } +} diff --git a/src/test/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculatorTest.java b/src/test/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculatorTest.java new file mode 100644 index 00000000..ecb64885 --- /dev/null +++ b/src/test/java/baseball/domain/strike_zone_calculator/StrikeZoneCalculatorTest.java @@ -0,0 +1,54 @@ +package baseball.domain.strike_zone_calculator; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import baseball.domain.entity.BallCountEntity; +import baseball.domain.entity.StrikeZoneResult; +import baseball.domain.score_calculator.BallCalculator; +import baseball.domain.score_calculator.IScoreCalculator; +import baseball.domain.score_calculator.StrikeCalculator; + +public class StrikeZoneCalculatorTest { + + IScoreCalculator strikeCalculator; + IScoreCalculator ballCalculator; + + IStrikeZoneCalculator strikeZoneCalculator; + + static List strikeZoneTestCases() { + return List.of( + Arguments.of(List.of(1, 2, 3), List.of(1, 2, 3), StrikeZoneResult.complete(new BallCountEntity(3, 0))), + Arguments.of(List.of(3, 2, 1), List.of(1, 2, 3), StrikeZoneResult.playing(new BallCountEntity(1, 2))), + Arguments.of(List.of(3, 1, 2), List.of(1, 2, 3), StrikeZoneResult.playing(new BallCountEntity(0, 3))), + Arguments.of(List.of(1, 5, 4), List.of(1, 2, 3), StrikeZoneResult.playing(new BallCountEntity(1, 0))), + Arguments.of(List.of(7, 5, 4), List.of(1, 2, 3), StrikeZoneResult.nothing()) + ); + } + + @BeforeEach + void setUp() { + this.strikeCalculator = new StrikeCalculator(); + this.ballCalculator = new BallCalculator(this.strikeCalculator); + + } + + @DisplayName("스트라이크 존 결과 테스트") + @ParameterizedTest + @MethodSource("strikeZoneTestCases") + void strikeZoneResult(List human, List computer, StrikeZoneResult expected) { + + var sut = new StrikeZoneCalculator(strikeCalculator, ballCalculator); + + var result = sut.execute(human, computer); + + assertThat(result).isEqualTo(expected); + } +} diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java new file mode 100644 index 00000000..e199bf86 --- /dev/null +++ b/src/test/java/study/StringTest.java @@ -0,0 +1,53 @@ +package study; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.platform.commons.util.StringUtils.*; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +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; + +public class StringTest { + + private Set numbers; + + @Test + void name() { + String name = "모두 반갑습니다."; + } + + @BeforeEach + void setUp() { + numbers = new HashSet<>(); + numbers.add(1); + numbers.add(2); + numbers.add(3); + } + + @Test + void contains() { + assertThat(numbers.contains(1)).isTrue(); + assertThat(numbers.contains(2)).isTrue(); + assertThat(numbers.contains(3)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) { + assertTrue(isBlank(input)); + } + + @ParameterizedTest + @CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':') + void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) { + String actualValue = input.toLowerCase(); + assertEquals(expected, actualValue); + } + +}