From bf3e08ec171c8a4a4d55839ec33ce369f1200e4c Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 14:14:02 +0900 Subject: [PATCH 01/20] =?UTF-8?q?refactor:=20ErrorMessage=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=ED=99=94=20(=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=ED=81=B4=201=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorMessage enum을 삭제하고 예외 발생 지점으로 메시지 이동 - 에러 메시지에 [ERROR] 접두사 추가 - InputView의 readHitOrStand 반환 타입을 boolean으로 변경해 컨트롤러 의존성 완화 - 예외 테스트 시의 구체적인 메시지 검증 제거 - Main 코드 가독성 개선 --- src/main/java/Main.java | 4 ++- src/main/java/common/ErrorMessage.java | 20 ------------- .../java/controller/BlackJackController.java | 3 +- src/main/java/domain/Deck.java | 3 +- src/main/java/domain/Name.java | 10 +------ src/main/java/domain/Players.java | 3 +- src/main/java/view/InputView.java | 28 +++++++++---------- src/test/java/domain/DeckTest.java | 7 ++--- src/test/java/domain/NameTest.java | 4 +-- src/test/java/domain/PlayersTest.java | 4 +-- 10 files changed, 24 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/common/ErrorMessage.java diff --git a/src/main/java/Main.java b/src/main/java/Main.java index e16bbbe943d..dd48579d88a 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -10,6 +10,8 @@ public static void main(String[] args) { OutputView outputView = new OutputView(); CardShuffleStrategy strategy = new RandomCardShuffleStrategy(); - new BlackJackController(inputView, outputView, strategy).doGame(); + BlackJackController controller = new BlackJackController(inputView, outputView, strategy); + + controller.doGame(); } } diff --git a/src/main/java/common/ErrorMessage.java b/src/main/java/common/ErrorMessage.java deleted file mode 100644 index 25df3bb9232..00000000000 --- a/src/main/java/common/ErrorMessage.java +++ /dev/null @@ -1,20 +0,0 @@ -package common; - -public enum ErrorMessage { - HIT_OR_STAND_VALUE_MIS_MATCH("y 혹은 n만 입력 가능합니다."), - DRAW_CARD_OUT_OF_RANGE("양수 이상의 숫자 중 남은 카드 수 만큼만 선택 가능"), - UNSUPPORTED_OPERATION_MESSAGE("해당 객체[%s]에서는 지원하지 않는 메서드입니다 "), - NOT_ALLOW_EMPTY_INPUT("공백은 허용되지 않습니다"), - ONLY_KO_AND_ENG("이름은 영어 또는 한국어만 가능합니다: "), - MAX_PLAYER_ERROR("최대 인원을 초과했습니다."); - - private final String message; - - ErrorMessage(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } -} diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index 14fa17861f2..d7ec94527a3 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -98,8 +98,7 @@ private boolean repeatAskDrawCardUntilSuccess(ParticipantDto playerDto) { private boolean askDrawCard(ParticipantDto participantDto) { outputView.printHitOrStandPrompt(participantDto); - String input = inputView.readHitOrStand(); - return input.equals("y"); + return inputView.readHitOrStand(); } private void drawCardAndPrintResult(Game game, Player player) { diff --git a/src/main/java/domain/Deck.java b/src/main/java/domain/Deck.java index 0a3bdccaa29..921c6f3fc32 100644 --- a/src/main/java/domain/Deck.java +++ b/src/main/java/domain/Deck.java @@ -1,6 +1,5 @@ package domain; -import common.ErrorMessage; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; @@ -38,7 +37,7 @@ private static void shuffleCards(List cards, CardShuffleStrategy strategy) public Card drawCard() { if (totalDeck.isEmpty()) { - throw new NoSuchElementException(ErrorMessage.DRAW_CARD_OUT_OF_RANGE.getMessage()); + throw new NoSuchElementException("[ERROR] 더 이상 뽑을 수 있는 카드가 없습니다."); } return totalDeck.removeFirst(); } diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java index 68bca5286d1..924ac521b04 100644 --- a/src/main/java/domain/Name.java +++ b/src/main/java/domain/Name.java @@ -1,25 +1,17 @@ package domain; -import common.ErrorMessage; import java.util.regex.Pattern; public record Name(String name) { private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z가-힣]+$"); public Name { - validateIsNotBlank(name); validateKoreanAndEnglish(name); } - private static void validateIsNotBlank(String name) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException(ErrorMessage.NOT_ALLOW_EMPTY_INPUT.getMessage()); - } - } - private static void validateKoreanAndEnglish(String name) { if (!NAME_PATTERN.matcher(name).matches()) { - throw new IllegalArgumentException(ErrorMessage.ONLY_KO_AND_ENG.getMessage()); + throw new IllegalArgumentException("[ERROR] 이름은 영어 또는 한국어만 가능합니다."); } } } diff --git a/src/main/java/domain/Players.java b/src/main/java/domain/Players.java index 1d4cffcbbdd..be950351078 100644 --- a/src/main/java/domain/Players.java +++ b/src/main/java/domain/Players.java @@ -1,6 +1,5 @@ package domain; -import common.ErrorMessage; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -23,7 +22,7 @@ public static Players of(List playerNames) { private static void validatePlayersCount(List playerNames) { if (playerNames.size() > MAX_PLAYER_NUMBER) { - throw new IllegalArgumentException(ErrorMessage.MAX_PLAYER_ERROR.getMessage()); + throw new IllegalArgumentException("[ERROR] 플레이 가능한 최대 인원을 초과했습니다."); } } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 1b6e3aa468f..9547036d38f 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -1,37 +1,35 @@ package view; -import common.ErrorMessage; import java.util.Arrays; import java.util.List; import java.util.Scanner; public class InputView { - private static final String DELIMITER = ","; - private static final List HIT_OR_STAND = List.of("y", "n"); private static final Scanner scanner = new Scanner(System.in); public List readNames() { String line = scanner.nextLine(); validateIsBlank(line); - return Arrays.stream(line.split(DELIMITER)).toList(); + List names = Arrays.stream(line.split("\\s*,\\s*")).toList(); + names.forEach(this::validateIsBlank); + return names; } - public String readHitOrStand() { + public boolean readHitOrStand() { String input = scanner.nextLine().trim(); validateIsBlank(input); - validateHitOrStandValue(input); - return input; - } - - private void validateIsBlank(String line) { - if (line.isBlank()) { - throw new IllegalArgumentException(ErrorMessage.NOT_ALLOW_EMPTY_INPUT.getMessage()); + if (input.equals("y")) { + return true; + } + if (input.equals("n")) { + return false; } + throw new IllegalArgumentException("[ERROR] y 혹은 n만 입력 가능합니다."); } - private void validateHitOrStandValue(String input) { - if (!HIT_OR_STAND.contains(input)) { - throw new IllegalArgumentException(ErrorMessage.HIT_OR_STAND_VALUE_MIS_MATCH.getMessage()); + private void validateIsBlank(String input) { + if (input.isBlank()) { + throw new IllegalArgumentException("[ERROR] 공백은 허용되지 않습니다"); } } } diff --git a/src/test/java/domain/DeckTest.java b/src/test/java/domain/DeckTest.java index c8646c5a78a..eb34c6cd016 100644 --- a/src/test/java/domain/DeckTest.java +++ b/src/test/java/domain/DeckTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import common.ErrorMessage; import java.util.NoSuchElementException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,8 +22,7 @@ void shouldReturnTotalDeckWithAllCards() { // then assertThatThrownBy(totalDeck::drawCard) - .isInstanceOf(NoSuchElementException.class) - .hasMessage(ErrorMessage.DRAW_CARD_OUT_OF_RANGE.getMessage()); + .isInstanceOf(NoSuchElementException.class); } @Test @@ -43,7 +41,6 @@ void shouldReturnSingleCardAndRemoveCardFromDeck() { // then assertThat(result).isEqualTo(expected); assertThatThrownBy(totalDeck::drawCard) - .isInstanceOf(NoSuchElementException.class) - .hasMessage(ErrorMessage.DRAW_CARD_OUT_OF_RANGE.getMessage()); + .isInstanceOf(NoSuchElementException.class); } } diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java index 782dc7f3239..ce3b1d141a2 100644 --- a/src/test/java/domain/NameTest.java +++ b/src/test/java/domain/NameTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class NameTest { @@ -22,8 +21,7 @@ void shouldReturnNameForKoreanOrEnglishName(String name) { @ParameterizedTest @DisplayName("이름이 공백이거나 공백이나 특수문자가 포함된 경우 오류가 발생한다.") - @NullAndEmptySource - @ValueSource(strings = {" ", "공 백문자포함", "특수문자포함!"}) + @ValueSource(strings = {"", " ", "공 백문자포함", "특수문자포함!"}) void shouldThrowExceptionForInvalidName(String name) { // when & then assertThatThrownBy( diff --git a/src/test/java/domain/PlayersTest.java b/src/test/java/domain/PlayersTest.java index 900d50b4097..68f45bbdb2b 100644 --- a/src/test/java/domain/PlayersTest.java +++ b/src/test/java/domain/PlayersTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import common.ErrorMessage; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,7 +28,6 @@ void shouldThrowExceptionWhenPlayerNumberOverMaximum() { // when & then assertThatThrownBy(() -> Players.of(testPlayerNames)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(ErrorMessage.MAX_PLAYER_ERROR.getMessage()); + .isInstanceOf(IllegalArgumentException.class); } } From 7f9f7b64da7635711851dc2246252a665e4c5fe6 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 17:12:21 +0900 Subject: [PATCH 02/20] =?UTF-8?q?refactor:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=BA=A1=EC=8A=90=ED=99=94=20=EB=B0=8F=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=A4=91=EC=95=99=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 내 불필요하게 열려있던 public 메서드를 private으로 변경해 캡슐화 강화 - 각 도메인과 InputView에 산발적으로 존재하던 "[ERROR]" 접두사 문자열을 제거하고, OutputView에서 에러 메시지를 출력할 때 일괄적으로 포맷팅하도록 구조 개선 --- src/main/java/controller/BlackJackController.java | 2 +- src/main/java/domain/Cards.java | 3 +-- src/main/java/domain/Deck.java | 2 +- src/main/java/domain/Name.java | 2 +- src/main/java/domain/Players.java | 2 +- src/main/java/view/InputView.java | 4 ++-- src/main/java/view/OutputView.java | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index d7ec94527a3..a6a4f8c95fa 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -119,7 +119,7 @@ private void checkAndAdjustDealerCards(Game game) { } } - public void showGameResult(Game game) { + private void showGameResult(Game game) { List participants = game.getParticipants(); List participantDtos = ParticipantDto.listOf(participants); outputView.printCardInfosWithSum(participantDtos); diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index 007abff0230..2764da1108e 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -18,8 +18,7 @@ public Cards() { public void addCard(Card card) { cards.add(card); } - - + public int calculateCardScoreSum() { int scoreSum = getScoreSumWithBasicAceScore(); if (hasAce() && canApplyMaxAceScore(scoreSum)) { diff --git a/src/main/java/domain/Deck.java b/src/main/java/domain/Deck.java index 921c6f3fc32..02688685054 100644 --- a/src/main/java/domain/Deck.java +++ b/src/main/java/domain/Deck.java @@ -37,7 +37,7 @@ private static void shuffleCards(List cards, CardShuffleStrategy strategy) public Card drawCard() { if (totalDeck.isEmpty()) { - throw new NoSuchElementException("[ERROR] 더 이상 뽑을 수 있는 카드가 없습니다."); + throw new NoSuchElementException("더 이상 뽑을 수 있는 카드가 없습니다."); } return totalDeck.removeFirst(); } diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java index 924ac521b04..68c591a1c03 100644 --- a/src/main/java/domain/Name.java +++ b/src/main/java/domain/Name.java @@ -11,7 +11,7 @@ public record Name(String name) { private static void validateKoreanAndEnglish(String name) { if (!NAME_PATTERN.matcher(name).matches()) { - throw new IllegalArgumentException("[ERROR] 이름은 영어 또는 한국어만 가능합니다."); + throw new IllegalArgumentException("이름은 영어 또는 한국어만 가능합니다."); } } } diff --git a/src/main/java/domain/Players.java b/src/main/java/domain/Players.java index be950351078..acb39a302bd 100644 --- a/src/main/java/domain/Players.java +++ b/src/main/java/domain/Players.java @@ -22,7 +22,7 @@ public static Players of(List playerNames) { private static void validatePlayersCount(List playerNames) { if (playerNames.size() > MAX_PLAYER_NUMBER) { - throw new IllegalArgumentException("[ERROR] 플레이 가능한 최대 인원을 초과했습니다."); + throw new IllegalArgumentException("플레이 가능한 최대 인원을 초과했습니다."); } } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 9547036d38f..08b718771f9 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -24,12 +24,12 @@ public boolean readHitOrStand() { if (input.equals("n")) { return false; } - throw new IllegalArgumentException("[ERROR] y 혹은 n만 입력 가능합니다."); + throw new IllegalArgumentException("y 혹은 n만 입력 가능합니다."); } private void validateIsBlank(String input) { if (input.isBlank()) { - throw new IllegalArgumentException("[ERROR] 공백은 허용되지 않습니다"); + throw new IllegalArgumentException("공백은 허용되지 않습니다"); } } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index 4e011b2f77a..6f0a840d45d 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -12,7 +12,7 @@ public class OutputView { private static final String DELIMITER = ", "; public void printErrorMessage(Exception e) { - System.out.println(e.getMessage()); + System.out.println("[ERROR] " + e.getMessage()); } public void printNamePrompt() { From 88f6547f7d9ba5b8cfa936a5e8c8f1fccf256d1e Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 17:16:32 +0900 Subject: [PATCH 03/20] =?UTF-8?q?docs:=20=EB=B8=94=EB=9E=99=EC=9E=AD=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EB=B0=B0=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=B0=8F=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B7=9C=EC=B9=99=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=EC=82=AC=EC=9D=B4=ED=81=B4=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 주요 도메인 용어(Blackjack, Bust, Hit/Stand) 정의 추가 - 추가된 배팅 기능을 구현하기 위한 세부 기능 목록 추가 - 사용자 입력값 유효성 검사와 도메인 규칙 검증의 책임을 명시적으로 분리 - 엣지 케이스 처리 정책 명시 --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1be485c8528..6cbd140a3a9 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,70 @@ 콘솔 기반으로 즐길 수 있는 블랙잭 게임입니다. +## 💡 주요 용어 + +- **Blackjack**: 처음 받은 카드 2장의 합이 21인 상태. (2장을 초과해 21을 만든 경우는 포함하지 않음) +- **Bust**: 카드의 점수 합이 21을 초과한 상태. (즉시 패배 처리) +- **Hit / Stand**: 카드를 추가로 1장 더 받을지(Hit), 여기서 턴을 종료할지(Stand) 결정하는 행위. + ## 🚀 기능 요구 사항 ### 1. 게임 준비 및 카드 분배 - [x] 참여할 플레이어들의 이름을 쉼표(,) 기준으로 입력받는다. - - 예외: 이름이 공백이거나 한글/영문이 아닌 경우. - - 예외: 최대 참여 인원(5명)을 초과하는 경우. + - [x] 사용자 입력값 유효성 검사 + - 이름 목록이 공백인 경우 예외 처리 + - 이름이 공백인 경우 예외 처리 + - [x] 도메인 규칙 검증 + - 최대 참여 인원(5명)을 초과하는 경우 예외 처리 + - 이름에 한글 또는 영문을 제외한 문자가 포함되는 경우 예외 처리 +- [ ] 각 플레이어에게 배팅 금액을 입력받는다. + - [ ] 사용자 입력값 유효성 검사 + - 배팅 금액이 공백인 경우 예외 처리 + - 숫자가 아닌 문자를 입력한 경우 예외 처리 + - [ ] 도메인 규칙 검증 + - 배팅 금액이 0 이하인 경우 예외 처리 + - 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - [x] 딜러와 플레이어들에게 무작위로 섞인 카드를 2장씩 나누어 준다. -- [x] 분배된 초기 카드를 출력한다. (딜러는 1장만 공개) +- [x] 분배된 초기 카드를 출력한다. + - 딜러는 1장만 공개 -### 2. 카드 추가 발급 (Hit / Stand) +### 2. 카드 추가 분배 (Hit / Stand) -- [x] 각 플레이어에게 카드를 더 받을지(y/n) 입력받는다. -- [x] 'y'를 입력하면 카드를 1장 추가로 지급하고 현재 카드를 출력한다. -- [x] 카드의 합이 21을 초과(Bust)하면 더 이상 카드를 받을 수 없다. -- [x] 딜러는 플레이어의 턴이 모두 끝난 후, 카드의 합이 16 이하이면 카드를 1장 더 받는다. (17 이상이면 받지 않음) +- [x] 플레이어는 카드 합계가 21 미만이라면 계속해서 카드를 추가로 받을 수 있다. + - [x] 사용자 입력값 유효성 검사 + - 대답이 공백인 경우 예외 처리 + - `y` 또는 `n` 이외의 값을 입력한 경우 예외 처리 + - [x] 입력값에 따라 Hit / Stand 의사 결정 + - `y` 입력 시: Hit + - `n` 입력 시: Stand + - [ ] 도메인 규칙 검증 + - 이미 점수가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 +- [x] 딜러는 자신의 카드 합계가 16 이하이면 무조건 1장을 더 받고, 17 이상이면 받지 않는다. +- [x] 딜러가 카드를 추가로 받았는지 여부를 출력한다. -### 3. 점수 계산 및 승패 판정 +### 3. 점수 계산 및 승패/수익 판정 - [x] J, Q, K는 10으로 계산한다. - [x] Ace(A)는 1 또는 11 중 카드 합산 범위(21)를 초과하지 않는 유리한 방향으로 계산한다. -- [x] 딜러와 플레이어의 점수를 비교하여 최종 승패를 결정한다. - - 딜러가 Bust인 경우, Bust되지 않은 플레이어는 모두 승리한다. - - 점수가 같을 경우 무승부로 처리한다. +- [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 최종 수익을 계산한다. + - [ ] 플레이어가 승리한 경우 + - 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 + - 딜러만 Bust인 경우: 배팅 금액의 1배 수익 + - 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 + - [ ] 플레이어가 패배한 경우: 배팅 금액만큼 손실 (-1배) + - 딜러만 Blackjack인 경우 + - 플레이어가 Bust인 경우 + - 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 + - [ ] 무승부인 경우: 수익 0 + - 플레이어와 딜러 모두 Blackjack인 경우 + - 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 +- [ ] 딜러의 수익은 플레이어들의 최종 수익 합의 반대로 계산한다. ### 4. 게임 결과 출력 - [x] 모든 참가자의 최종 카드 목록과 점수를 출력한다. -- [x] 딜러와 플레이어들의 최종 승패(승/무/패)를 출력한다. +- [ ] 딜러와 플레이어들의 최종 수익을 출력한다. ## 🗂️ 클래스 다이어그램 From a2a605c47a7b443e4677db5c773641c400e05324 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 17:42:47 +0900 Subject: [PATCH 04/20] =?UTF-8?q?docs:=20=EB=B0=B0=ED=8C=85=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EC=9D=98=20=EC=B1=85=EC=9E=84=20=EC=9E=AC=EB=B6=84=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '정수 범위 초과' 예외를 도메인 규칙 검증에서 사용자 입력값 유효성 검사로 이동 - 데이터 타입 변환의 책임은 InputView에, 비즈니스 규칙은 도메인에 있도록 책임을 명확히 분리 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cbd140a3a9..ce815c8e536 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ - [ ] 사용자 입력값 유효성 검사 - 배팅 금액이 공백인 경우 예외 처리 - 숫자가 아닌 문자를 입력한 경우 예외 처리 + - 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - [ ] 도메인 규칙 검증 - 배팅 금액이 0 이하인 경우 예외 처리 - - 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - [x] 딜러와 플레이어들에게 무작위로 섞인 카드를 2장씩 나누어 준다. - [x] 분배된 초기 카드를 출력한다. - 딜러는 1장만 공개 From a2c3fcdc94e4f6e281b1501efe1e0cc80fd4b07c Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 18:38:06 +0900 Subject: [PATCH 05/20] =?UTF-8?q?docs:=20Ace=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A0=90=EC=88=98=20=EA=B3=84=EC=82=B0=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B7=9C=EC=B9=99=20=EB=AA=85=EC=84=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드 상에 구현되어 있는 Ace 카드의 유동적 점수 계산(1점 / 11점 판별) 로직을 기능 명세에 구체적으로 반영 - Ace 카드가 여러 장 포함될 경우의 한계(11점으로 계산 가능한 Ace는 최대 1장) 등 엣지 케이스 명시 - 코드와 기능 요구 사항 문서의 동기화를 통해 도메인 규칙의 가시성 확보 --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce815c8e536..08cfc6d579a 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,23 @@ - `y` 입력 시: Hit - `n` 입력 시: Stand - [ ] 도메인 규칙 검증 - - 이미 점수가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 -- [x] 딜러는 자신의 카드 합계가 16 이하이면 무조건 1장을 더 받고, 17 이상이면 받지 않는다. + - 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 +- [x] 딜러는 자신의 카드 합계가 16 이하이면 17 이상이 될 때까지 무조건 1장을 더 받는다. + - [ ] 도메인 규칙 검증 + - 이미 딜러의 카드 합계가 17 이상이라 더 이상 받을 수 없는 경우 예외 처리 + - 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 - [x] 딜러가 카드를 추가로 받았는지 여부를 출력한다. ### 3. 점수 계산 및 승패/수익 판정 - [x] J, Q, K는 10으로 계산한다. - [x] Ace(A)는 1 또는 11 중 카드 합산 범위(21)를 초과하지 않는 유리한 방향으로 계산한다. + - [x] Ace가 1장 포함된 경우 + - Ace를 11점으로 계산해도 Bust가 나지 않는다면, Ace를 11점으로 계산 + - Ace를 11점으로 계산 시 Bust가 난다면, Ace를 1점으로 계산 + - [x] Ace가 2장 이상 포함된 경우 + - 2장 이상을 11로 계산하면 무조건 22점 이상으로 Bust가 나므로, 11로 계산할 수 있는 Ace는 최대 1장 + - 1장의 Ace를 11로 계산했을 때 Bust가 난다면, 모든 Ace를 1점으로 계산 - [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 최종 수익을 계산한다. - [ ] 플레이어가 승리한 경우 - 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 From d87d40bf9b40d17ed9316ea8f7a96e7be4a1b6e2 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 19:12:38 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20Cards=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=B9=B4=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 21점 이상일 때 카드를 추가할 수 없는 도메인 규칙 예외 발생 로직 및 검증 추가 - 경계값(21점 미만, 21점, 21점 초과)에 따른 상태 판정 로직 테스트 작성 - 카드 생성 private 메서드 도입으로 테스트 코드 중복 개선 --- README.md | 2 +- src/main/java/domain/Cards.java | 5 +- src/test/java/domain/CardsTest.java | 137 +++++++++++++++++++++------- 3 files changed, 108 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 08cfc6d579a..c7dd0ca1e82 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ - [x] 입력값에 따라 Hit / Stand 의사 결정 - `y` 입력 시: Hit - `n` 입력 시: Stand - - [ ] 도메인 규칙 검증 + - [x] 도메인 규칙 검증 - 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 - [x] 딜러는 자신의 카드 합계가 16 이하이면 17 이상이 될 때까지 무조건 1장을 더 받는다. - [ ] 도메인 규칙 검증 diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index 2764da1108e..4a94e8eb955 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -16,9 +16,12 @@ public Cards() { } public void addCard(Card card) { + if (calculateCardScoreSum() >= BUST_CRITERIA) { + throw new IllegalStateException("이미 카드 합계가 21 이상입니다. 카드를 추가할 수 없습니다."); + } cards.add(card); } - + public int calculateCardScoreSum() { int scoreSum = getScoreSumWithBasicAceScore(); if (hasAce() && canApplyMaxAceScore(scoreSum)) { diff --git a/src/test/java/domain/CardsTest.java b/src/test/java/domain/CardsTest.java index 1cab9fd6559..080a689b475 100644 --- a/src/test/java/domain/CardsTest.java +++ b/src/test/java/domain/CardsTest.java @@ -1,6 +1,7 @@ package domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,15 +10,75 @@ import org.junit.jupiter.api.Test; class CardsTest { + private Cards createCardsWithCards(Card... cards) { + Cards result = new Cards(); + for (Card card : cards) { + result.addCard(card); + } + return result; + } + + @Nested + class AddCardTest { + @Test + @DisplayName("카드 합계가 21점 미만일 때 카드를 정상적으로 추가할 수 있다.") + void shouldAddCardWhenDeckSumLessThanMaximum() { + // given + Cards cards = new Cards(); + cards.addCard(new Card(CardShape.SPADE, CardContents.TWO)); + + // when + cards.addCard(new Card(CardShape.HEART, CardContents.THREE)); + + // then + assertThat(cards.getCards()).hasSize(2); + } + + @Test + @DisplayName("카드 합계가 21점일 때 카드를 추가로 뽑으면 예외가 발생한다.") + void shouldThrowExceptionWhenDeckSumEqualsMaximum() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.TEN), + new Card(CardShape.DIAMOND, CardContents.A) + ); + + Card newCard = new Card(CardShape.HEART, CardContents.THREE); + + // when & then + assertThatThrownBy(() -> cards.addCard(newCard)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("카드 합계가 21점을 초과할 때 카드를 추가로 뽑으면 예외가 발생한다.") + void shouldThrowExceptionWhenDeckSumOverMaximum() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.TEN), + new Card(CardShape.DIAMOND, CardContents.TWO) + ); + + Card newCard = new Card(CardShape.HEART, CardContents.THREE); + + // when & then + assertThatThrownBy(() -> cards.addCard(newCard)) + .isInstanceOf(IllegalStateException.class); + } + } + @Nested class IsLessThanMaxScoreTest { @Test @DisplayName("카드의 합이 21점 미만이면 true를 반환한다.") void shouldReturnTrueWhenDeckSumLessThanMaximum() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN) + ); // when & then assertTrue(cards.isLessThanMaxScore()); @@ -27,10 +88,11 @@ void shouldReturnTrueWhenDeckSumLessThanMaximum() { @DisplayName("카드의 합이 정확히 21점이면 false를 반환한다.") void shouldReturnFalseWhenDeckSumEqualsMaximum() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); - cards.addCard(new Card(CardShape.CLOVER, CardContents.A)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.A) + ); // when & then assertFalse(cards.isLessThanMaxScore()); @@ -40,10 +102,11 @@ void shouldReturnFalseWhenDeckSumEqualsMaximum() { @DisplayName("카드의 합이 21점을 초과하면 false를 반환한다.") void shouldReturnFalseWhenDeckSumOverMaximum() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); - cards.addCard(new Card(CardShape.CLOVER, CardContents.TWO)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.TWO) + ); // when & then assertFalse(cards.isLessThanMaxScore()); @@ -56,10 +119,11 @@ class IsBustTest { @DisplayName("카드의 합이 21을 초과하면 버스트로 판정한다.") void shouldReturnTrueWhenDeckSumOverMaximum() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); - cards.addCard(new Card(CardShape.CLOVER, CardContents.TEN)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.TEN) + ); // when & then assertTrue(cards.isBust()); @@ -69,10 +133,11 @@ void shouldReturnTrueWhenDeckSumOverMaximum() { @DisplayName("카드의 합이 21이하라면 버스트로 판정하지 않는다.") void shouldReturnFalseWhenDeckSumEqualsMaximumOrLess() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.A)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.A) + ); // when & then assertFalse(cards.isBust()); @@ -85,9 +150,10 @@ class CalculateCardScoreSumTest { @DisplayName("Ace가 없는 경우 카드 점수의 합을 정확히 계산한다.") void shouldReturnScoreSumWithoutAce() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN) + ); // when & then assertThat(cards.calculateCardScoreSum()).isEqualTo(20); @@ -97,9 +163,10 @@ void shouldReturnScoreSumWithoutAce() { @DisplayName("Ace가 포함되어 있고 11점으로 계산해도 버스트가 나지 않는다면, Ace를 11점으로 계산한다.") void shouldReturnScoreSumWithAceCalculatedAsElevenWhenNotBust() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.A)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.A) + ); // when & then assertThat(cards.calculateCardScoreSum()).isEqualTo(21); @@ -109,10 +176,11 @@ void shouldReturnScoreSumWithAceCalculatedAsElevenWhenNotBust() { @DisplayName("Ace가 포함되어 있으나 11점으로 계산 시 버스트가 난다면, Ace를 1점으로 계산한다.") void shouldReturnScoreSumWithAceCalculatedAsOneWhenElevenCausesBust() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.TEN)); - cards.addCard(new Card(CardShape.HEART, CardContents.A)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.A) + ); // when & then assertThat(cards.calculateCardScoreSum()).isEqualTo(21); @@ -122,11 +190,12 @@ void shouldReturnScoreSumWithAceCalculatedAsOneWhenElevenCausesBust() { @DisplayName("Ace가 2장 이상일 때, 최대 1장만 11점으로 계산하고 나머지는 1점으로 계산한다.") void shouldReturnScoreSumWithMultiplyAces() { // given - Cards cards = new Cards(); - cards.addCard(new Card(CardShape.SPADE, CardContents.A)); - cards.addCard(new Card(CardShape.HEART, CardContents.A)); - cards.addCard(new Card(CardShape.CLOVER, CardContents.A)); - cards.addCard(new Card(CardShape.DIAMOND, CardContents.TEN)); + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.A), + new Card(CardShape.HEART, CardContents.A), + new Card(CardShape.CLOVER, CardContents.A), + new Card(CardShape.DIAMOND, CardContents.TEN) + ); // when & then assertThat(cards.calculateCardScoreSum()).isEqualTo(13); From 45abe83cffe9ec3d58edbe8c318c35f00279c617 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 19:46:01 +0900 Subject: [PATCH 07/20] =?UTF-8?q?docs:=20=EC=B9=B4=EB=93=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=B6=94=EA=B0=80=20=EB=B0=A9=EC=A7=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B7=9C=EC=B9=99=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 보유하고 있는 동일한 카드를 중복해서 추가하려는 경우의 예외 처리 명시 - 카드를 추가로 받을 때마다 사전 검증되어야 하는 공통 확인 사항을 명시적으로 목록화 - 예외 처리 및 상세 조건 등 하위 항목까지 체크박스를 부여해, 기능 구현 및 테스트 진행도를 더욱 세밀하게 확인할 수 있도록 문서 양식 개선 --- README.md | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c7dd0ca1e82..5342d804be3 100644 --- a/README.md +++ b/README.md @@ -20,37 +20,39 @@ - [x] 참여할 플레이어들의 이름을 쉼표(,) 기준으로 입력받는다. - [x] 사용자 입력값 유효성 검사 - - 이름 목록이 공백인 경우 예외 처리 - - 이름이 공백인 경우 예외 처리 + - [x] 이름 목록이 공백인 경우 예외 처리 + - [x] 이름이 공백인 경우 예외 처리 - [x] 도메인 규칙 검증 - - 최대 참여 인원(5명)을 초과하는 경우 예외 처리 - - 이름에 한글 또는 영문을 제외한 문자가 포함되는 경우 예외 처리 + - [x] 최대 참여 인원(5명)을 초과하는 경우 예외 처리 + - [x] 이름에 한글 또는 영문을 제외한 문자가 포함되는 경우 예외 처리 - [ ] 각 플레이어에게 배팅 금액을 입력받는다. - [ ] 사용자 입력값 유효성 검사 - - 배팅 금액이 공백인 경우 예외 처리 - - 숫자가 아닌 문자를 입력한 경우 예외 처리 - - 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 + - [ ] 배팅 금액이 공백인 경우 예외 처리 + - [ ] 숫자가 아닌 문자를 입력한 경우 예외 처리 + - [ ] 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - [ ] 도메인 규칙 검증 - - 배팅 금액이 0 이하인 경우 예외 처리 + - [ ] 배팅 금액이 0 이하인 경우 예외 처리 - [x] 딜러와 플레이어들에게 무작위로 섞인 카드를 2장씩 나누어 준다. - [x] 분배된 초기 카드를 출력한다. - 딜러는 1장만 공개 ### 2. 카드 추가 분배 (Hit / Stand) +- [x] 카드 추가 분배는 다음의 공통 제약 조건 내에서만 가능하다. + - [ ] 도메인 규칙 검증 + - [x] 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 + - [ ] 이미 보유하고 있는 카드와 동일한 카드를 중복해서 추가하려는 경우 예외 처리 - [x] 플레이어는 카드 합계가 21 미만이라면 계속해서 카드를 추가로 받을 수 있다. - [x] 사용자 입력값 유효성 검사 - - 대답이 공백인 경우 예외 처리 - - `y` 또는 `n` 이외의 값을 입력한 경우 예외 처리 + - [x] 대답이 공백인 경우 예외 처리 + - [x] `y` 또는 `n` 이외의 값을 입력한 경우 예외 처리 - [x] 입력값에 따라 Hit / Stand 의사 결정 - `y` 입력 시: Hit - `n` 입력 시: Stand - - [x] 도메인 규칙 검증 - - 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 - [x] 딜러는 자신의 카드 합계가 16 이하이면 17 이상이 될 때까지 무조건 1장을 더 받는다. - [ ] 도메인 규칙 검증 - - 이미 딜러의 카드 합계가 17 이상이라 더 이상 받을 수 없는 경우 예외 처리 - - 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 + - [ ] 이미 딜러의 카드 합계가 17 이상이라 더 이상 받을 수 없는 경우 예외 처리 + - [ ] 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 - [x] 딜러가 카드를 추가로 받았는지 여부를 출력한다. ### 3. 점수 계산 및 승패/수익 판정 @@ -58,23 +60,23 @@ - [x] J, Q, K는 10으로 계산한다. - [x] Ace(A)는 1 또는 11 중 카드 합산 범위(21)를 초과하지 않는 유리한 방향으로 계산한다. - [x] Ace가 1장 포함된 경우 - - Ace를 11점으로 계산해도 Bust가 나지 않는다면, Ace를 11점으로 계산 - - Ace를 11점으로 계산 시 Bust가 난다면, Ace를 1점으로 계산 + - [x] Ace를 11점으로 계산해도 Bust가 나지 않는다면, Ace를 11점으로 계산 + - [x] Ace를 11점으로 계산 시 Bust가 난다면, Ace를 1점으로 계산 - [x] Ace가 2장 이상 포함된 경우 - 2장 이상을 11로 계산하면 무조건 22점 이상으로 Bust가 나므로, 11로 계산할 수 있는 Ace는 최대 1장 - - 1장의 Ace를 11로 계산했을 때 Bust가 난다면, 모든 Ace를 1점으로 계산 + - [x] 1장의 Ace를 11로 계산했을 때 Bust가 난다면, 모든 Ace를 1점으로 계산 - [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 최종 수익을 계산한다. - [ ] 플레이어가 승리한 경우 - - 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 - - 딜러만 Bust인 경우: 배팅 금액의 1배 수익 - - 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 + - [ ] 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 + - [ ] 딜러만 Bust인 경우: 배팅 금액의 1배 수익 + - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 - [ ] 플레이어가 패배한 경우: 배팅 금액만큼 손실 (-1배) - - 딜러만 Blackjack인 경우 - - 플레이어가 Bust인 경우 - - 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 + - [ ] 딜러만 Blackjack인 경우 + - [ ] 플레이어가 Bust인 경우 + - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 - [ ] 무승부인 경우: 수익 0 - - 플레이어와 딜러 모두 Blackjack인 경우 - - 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 + - [ ] 플레이어와 딜러 모두 Blackjack인 경우 + - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 - [ ] 딜러의 수익은 플레이어들의 최종 수익 합의 반대로 계산한다. ### 4. 게임 결과 출력 From a1648e63b700ebfa4d83fe1bc7fa63d7628fe545 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 20:12:33 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat(Cards):=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/main/java/domain/Cards.java | 9 ++++++++- src/test/java/domain/CardsTest.java | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5342d804be3..86a8ab55356 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ ### 2. 카드 추가 분배 (Hit / Stand) - [x] 카드 추가 분배는 다음의 공통 제약 조건 내에서만 가능하다. - - [ ] 도메인 규칙 검증 + - [x] 도메인 규칙 검증 - [x] 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 - - [ ] 이미 보유하고 있는 카드와 동일한 카드를 중복해서 추가하려는 경우 예외 처리 + - [x] 이미 보유하고 있는 카드와 동일한 카드를 중복해서 추가하려는 경우 예외 처리 - [x] 플레이어는 카드 합계가 21 미만이라면 계속해서 카드를 추가로 받을 수 있다. - [x] 사용자 입력값 유효성 검사 - [x] 대답이 공백인 경우 예외 처리 diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index 4a94e8eb955..c05e0b7a924 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -16,10 +16,17 @@ public Cards() { } public void addCard(Card card) { + validateAddable(card); + cards.add(card); + } + + private void validateAddable(Card card) { if (calculateCardScoreSum() >= BUST_CRITERIA) { throw new IllegalStateException("이미 카드 합계가 21 이상입니다. 카드를 추가할 수 없습니다."); } - cards.add(card); + if (cards.contains(card)) { + throw new IllegalArgumentException("이미 보유하고 있는 카드입니다. 중복 추가할 수 없습니다."); + } } public int calculateCardScoreSum() { diff --git a/src/test/java/domain/CardsTest.java b/src/test/java/domain/CardsTest.java index 080a689b475..ad6fab9f9ae 100644 --- a/src/test/java/domain/CardsTest.java +++ b/src/test/java/domain/CardsTest.java @@ -67,6 +67,21 @@ void shouldThrowExceptionWhenDeckSumOverMaximum() { assertThatThrownBy(() -> cards.addCard(newCard)) .isInstanceOf(IllegalStateException.class); } + + @Test + @DisplayName("카드 목록에 이미 존재하는 카드를 중복으로 추가하면 예외가 발생한다.") + void shouldThrowExceptionForDuplicatedCard() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.DIAMOND, CardContents.TWO) + ); + + Card duplicatedCard = new Card(CardShape.DIAMOND, CardContents.TWO); + + // when & then + assertThatThrownBy(() -> cards.addCard(duplicatedCard)) + .isInstanceOf(IllegalArgumentException.class); + } } @Nested From 1b1c706e22d767a7632548641de1d65a61390068 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 20:43:14 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat(Dealer):=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=A9=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/main/java/domain/Dealer.java | 12 +++ src/test/java/domain/DealerTest.java | 117 ++++++++++++++++++++++----- 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 86a8ab55356..b57af8f63de 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ - [x] 입력값에 따라 Hit / Stand 의사 결정 - `y` 입력 시: Hit - `n` 입력 시: Stand -- [x] 딜러는 자신의 카드 합계가 16 이하이면 17 이상이 될 때까지 무조건 1장을 더 받는다. +- [x] 딜러는 자신의 카드 합계가 16 이하이면 16을 초과할 때까지 무조건 1장을 더 받는다. - [ ] 도메인 규칙 검증 - - [ ] 이미 딜러의 카드 합계가 17 이상이라 더 이상 받을 수 없는 경우 예외 처리 + - [x] 이미 딜러의 카드 합계가 16을 초과해 더 이상 받을 수 없는 경우 예외 처리 - [ ] 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 - [x] 딜러가 카드를 추가로 받았는지 여부를 출력한다. diff --git a/src/main/java/domain/Dealer.java b/src/main/java/domain/Dealer.java index 19f72f83b5a..56723eab567 100644 --- a/src/main/java/domain/Dealer.java +++ b/src/main/java/domain/Dealer.java @@ -20,4 +20,16 @@ public List getInitialVisibleCards() { public boolean isDrawable() { return super.getCardsSum() <= MINIMUM_TOTAL_SCORE; } + + @Override + public void addCard(Card card) { + validateAddable(); + super.addCard(card); + } + + private void validateAddable() { + if (super.getCardsSum() > MINIMUM_TOTAL_SCORE) { + throw new IllegalStateException("딜러는 카드 합계가 16을 초과할 시, 카드를 더 받을 수 없습니다."); + } + } } diff --git a/src/test/java/domain/DealerTest.java b/src/test/java/domain/DealerTest.java index c9f6203aa02..36fa570cfb0 100644 --- a/src/test/java/domain/DealerTest.java +++ b/src/test/java/domain/DealerTest.java @@ -1,10 +1,13 @@ package domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class DealerTest { @@ -29,30 +32,102 @@ void shouldReturnSingleCardForInitialVisibleCards() { .containsExactly(card1); } - @Test - @DisplayName("딜러는 카드의 합이 16 이하면 카드를 더 뽑을 수 있다.") - void shouldReturnTrueWhenCardScoreSumIsMinimumOrLess() { - // given - Dealer testDealer = createDealerWithCards( - new Card(CardShape.HEART, CardContents.FIVE), - new Card(CardShape.HEART, CardContents.SIX) - ); + @Nested + class IsDrawableTest { + @Test + @DisplayName("딜러는 카드의 합이 16 미만이면 카드를 더 뽑을 수 있다.") + void shouldReturnTrueWhenCardScoreSumLessThanMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.FIVE), + new Card(CardShape.HEART, CardContents.SIX) + ); - // when & then - assertTrue(testDealer.isDrawable()); + // when & then + assertTrue(testDealer.isDrawable()); + } + + @Test + @DisplayName("딜러는 카드의 합이 16이면 카드를 더 뽑을 수 있다.") + void shouldReturnTrueWhenCardScoreSumEqualsMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.SIX) + ); + + // when & then + assertTrue(testDealer.isDrawable()); + } + + @Test + @DisplayName("딜러는 카드의 합이 16을 초과하면 카드를 더 이상 뽑을 수 없다.") + void shouldReturnFalseWheCardScoreSumOverMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.SIX), + new Card(CardShape.SPADE, CardContents.FIVE) + ); + + // when & then + assertFalse(testDealer.isDrawable()); + } } - @Test - @DisplayName("딜러는 카드의 합이 16 이상이면 카드를 더 이상 뽑을 수 없다.") - void shouldReturnFalseWhenNotBust() { - // given - Dealer testDealer = createDealerWithCards( - new Card(CardShape.HEART, CardContents.TEN), - new Card(CardShape.CLOVER, CardContents.TEN), - new Card(CardShape.SPADE, CardContents.TEN) - ); + @Nested + class AddCardTest { + @Test + @DisplayName("딜러는 카드의 합이 16 미만일 때 카드를 정상적으로 추가할 수 있다.") + void shouldAddCardWhenDeckSumLessThanMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.FIVE), + new Card(CardShape.HEART, CardContents.SIX) + ); + Card newCard = new Card(CardShape.CLOVER, CardContents.SIX); - // when & then - assertFalse(testDealer.isDrawable()); + // when + testDealer.addCard(newCard); + + // then + List cards = testDealer.getCards(); + assertThat(cards).hasSize(3); + assertThat(cards).contains(newCard); + } + + @Test + @DisplayName("딜러는 카드의 합이 16일 때 카드를 정상적으로 추가할 수 있다.") + void shouldAddCardWhenDeckSumEqualsMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.SIX) + ); + Card newCard = new Card(CardShape.CLOVER, CardContents.SIX); + + // when + testDealer.addCard(newCard); + + // then + List cards = testDealer.getCards(); + assertThat(cards).hasSize(3); + assertThat(cards).contains(newCard); + } + + @Test + @DisplayName("딜러는 카드의 합이 16을 초과하면 카드를 더 이상 뽑을 수 없다.") + void shouldThrowExceptionWhenDeckSumOverMinimum() { + // given + Dealer testDealer = createDealerWithCards( + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.EIGHT) + ); + Card newCard = new Card(CardShape.CLOVER, CardContents.SIX); + + // when & then + assertThatThrownBy(() -> testDealer.addCard(newCard)) + .isInstanceOf(IllegalStateException.class); + } } } From e22a93e219fd122e9fea9b59711c949f2708e33f Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sat, 14 Mar 2026 21:15:13 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat(GameResult):=20=EB=94=9C=EB=9F=AC=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EA=B3=84=EC=82=B0=20=EB=B0=A9=EC=A7=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/main/java/domain/GameResult.java | 7 +++ src/test/java/domain/GameResultTest.java | 60 ++++++++++++++++++++---- src/test/java/domain/GameTest.java | 12 ++--- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b57af8f63de..82c4f8e06dc 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,9 @@ - `y` 입력 시: Hit - `n` 입력 시: Stand - [x] 딜러는 자신의 카드 합계가 16 이하이면 16을 초과할 때까지 무조건 1장을 더 받는다. - - [ ] 도메인 규칙 검증 + - [x] 도메인 규칙 검증 - [x] 이미 딜러의 카드 합계가 16을 초과해 더 이상 받을 수 없는 경우 예외 처리 - - [ ] 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 + - [x] 딜러의 카드 합계가 16 이하인 상태로 게임이 종료되는 경우 예외 처리 - [x] 딜러가 카드를 추가로 받았는지 여부를 출력한다. ### 3. 점수 계산 및 승패/수익 판정 diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java index 1695d9d5a0b..c9578b77519 100644 --- a/src/main/java/domain/GameResult.java +++ b/src/main/java/domain/GameResult.java @@ -15,11 +15,18 @@ private GameResult(Map playerResults, Map deale } public static GameResult calculate(Dealer dealer, Players players) { + validateCalculatable(dealer); Map playerResults = calculatePlayerResults(dealer, players); Map dealerResult = calculateDealerResult(playerResults); return new GameResult(playerResults, dealerResult); } + private static void validateCalculatable(Dealer dealer) { + if (dealer.isDrawable()) { + throw new IllegalStateException("딜러의 카드 합계가 16 이하이므로 아직 게임 결과를 계산할 수 없습니다."); + } + } + private static Map calculatePlayerResults(Dealer dealer, Players players) { boolean isDealerBust = dealer.isBust(); int dealerScore = dealer.getCardsSum(); diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java index 6d02d9f346f..2e8029a9257 100644 --- a/src/test/java/domain/GameResultTest.java +++ b/src/test/java/domain/GameResultTest.java @@ -1,21 +1,30 @@ package domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class GameResultTest { + + private void addCardsToParticipantDeck(Participant participant, Card... cards) { + for (Card card : cards) { + participant.addCard(card); + } + } + @Test @DisplayName("최종 승패 결과 계산이 정확하게 수행된다.") void shouldReturnWinTieLossResult() { // given Dealer dealer = new Dealer(); - addCardsToPlayerDeck(dealer, + addCardsToParticipantDeck(dealer, new Card(CardShape.HEART, CardContents.J), new Card(CardShape.HEART, CardContents.EIGHT) ); @@ -25,22 +34,22 @@ void shouldReturnWinTieLossResult() { Iterator playerIterator = players.iterator(); Player player1 = playerIterator.next(); - addCardsToPlayerDeck(player1, + addCardsToParticipantDeck(player1, new Card(CardShape.DIAMOND, CardContents.J), new Card(CardShape.DIAMOND, CardContents.THREE) ); Player player2 = playerIterator.next(); - addCardsToPlayerDeck(player2, + addCardsToParticipantDeck(player2, new Card(CardShape.SPADE, CardContents.J), new Card(CardShape.SPADE, CardContents.THREE) ); Player player3 = playerIterator.next(); - addCardsToPlayerDeck(player3, + addCardsToParticipantDeck(player3, new Card(CardShape.CLOVER, CardContents.J), new Card(CardShape.CLOVER, CardContents.NINE) ); Player player4 = playerIterator.next(); - addCardsToPlayerDeck(player4, + addCardsToParticipantDeck(player4, new Card(CardShape.HEART, CardContents.K), new Card(CardShape.HEART, CardContents.NINE) ); @@ -61,13 +70,44 @@ void shouldReturnWinTieLossResult() { Map dealerResults = gameResult.getDealerResult(); // then - assertThat(playerResults).containsAllEntriesOf(expectPlayerWinLossResults); - assertThat(dealerResults).containsAllEntriesOf(expectDealerWinLossResults); + assertThat(playerResults).isEqualTo(expectPlayerWinLossResults); + assertThat(dealerResults).isEqualTo(expectDealerWinLossResults); } - private void addCardsToPlayerDeck(Participant participant, Card... cards) { - for (Card card : cards) { - participant.addCard(card); + @Nested + class GenerateGameResultTest { + @Test + @DisplayName("딜러의 카드 합계가 16 미만인 상태로 게임 결과 계산을 시도하는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDealerScoreUnderMinimum() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TWO), + new Card(CardShape.CLOVER, CardContents.THREE) + ); + List playerNames = List.of("pobi", "terry"); + Players players = Players.of(playerNames); + + // when & then + assertThatThrownBy(() -> GameResult.calculate(dealer, players)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("딜러의 카드 합계가 16인 상태로 게임 결과 계산을 시도하는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDealerScoreEqualsMinimum() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.SIX) + ); + List playerNames = List.of("pobi", "terry"); + Players players = Players.of(playerNames); + + // when & then + assertThatThrownBy(() -> GameResult.calculate(dealer, players)) + .isInstanceOf(IllegalStateException.class); } } } diff --git a/src/test/java/domain/GameTest.java b/src/test/java/domain/GameTest.java index ea329ce927f..e024f1c7cad 100644 --- a/src/test/java/domain/GameTest.java +++ b/src/test/java/domain/GameTest.java @@ -14,6 +14,12 @@ class GameTest { }; private Game game; + private void addCardsToPlayerDeck(Participant participant, Card... cards) { + for (Card card : cards) { + participant.addCard(card); + } + } + @BeforeEach void setUp() { game = Game.registerParticipantsAndPrepareTotalDeck(PLAYER_NAMES, FIXED_SHUFFLE_STRATEGY); @@ -64,10 +70,4 @@ void shouldReturnTrueWhenParticipantsCanDrawCardUnderCondition() { assertTrue(dealerResult); assertTrue(playerResult); } - - private void addCardsToPlayerDeck(Participant participant, Card... cards) { - for (Card card : cards) { - participant.addCard(card); - } - } } From 19665785acd789ebbc4e3da239671c879a963202 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 01:05:45 +0900 Subject: [PATCH 11/20] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=96=B4=EC=A0=81?= =?UTF-8?q?=20=EB=B3=B5=EC=82=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EB=B6=88=EB=B3=80=EC=84=B1=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 Collections.unmodifiableList 의 'View' 방식에서 'Snapshot' 방식인 List.copyOf로 변경 - 원본 리스트의 변경이 도메인 객체 내부 리스트에 영향을 주지 않도록 원본과의 참조 연결을 완전히 차단 --- src/main/java/domain/Cards.java | 14 ++++++-------- src/main/java/domain/GameResult.java | 5 ++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index c05e0b7a924..5f3d8d7ff4e 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -1,11 +1,9 @@ package domain; import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; import java.util.List; -public class Cards implements Iterable { +public class Cards { private static final int BUST_CRITERIA = 21; private static final int ACE_EXTRA_SCORE = 10; @@ -57,12 +55,12 @@ public boolean isBust() { return calculateCardScoreSum() > BUST_CRITERIA; } - @Override - public Iterator iterator() { - return Collections.unmodifiableList(cards).iterator(); - } +// @Override +// public Iterator iterator() { +// return List.copyOf(cards).iterator(); +// } public List getCards() { - return Collections.unmodifiableList(cards); + return List.copyOf(cards); } } diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java index c9578b77519..9bb5d5f946e 100644 --- a/src/main/java/domain/GameResult.java +++ b/src/main/java/domain/GameResult.java @@ -1,6 +1,5 @@ package domain; -import java.util.Collections; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; @@ -57,10 +56,10 @@ private static Map calculateDealerResult(Map pl } public Map getPlayerResults() { - return Collections.unmodifiableMap(playerResults); + return Map.copyOf(playerResults); } public Map getDealerResult() { - return Collections.unmodifiableMap(dealerResult); + return Map.copyOf(dealerResult); } } From 144eb8cffd54f9a328f3f8aabb459972d5f95091 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 01:14:27 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat(BetMoney):=20=EB=B0=B0=ED=8C=85=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배팅 금액을 관리하는 BetMoney 객체 구현 - 0 이하의 금액 입력 시 예외 처리 로직 추가 --- README.md | 4 ++-- src/main/java/domain/BetMoney.java | 17 +++++++++++++ src/test/java/domain/BetMoneyTest.java | 33 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/main/java/domain/BetMoney.java create mode 100644 src/test/java/domain/BetMoneyTest.java diff --git a/README.md b/README.md index 82c4f8e06dc..d2465f8fa9c 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ - [ ] 배팅 금액이 공백인 경우 예외 처리 - [ ] 숫자가 아닌 문자를 입력한 경우 예외 처리 - [ ] 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - - [ ] 도메인 규칙 검증 - - [ ] 배팅 금액이 0 이하인 경우 예외 처리 + - [x] 도메인 규칙 검증 + - [x] 배팅 금액이 0 이하인 경우 예외 처리 - [x] 딜러와 플레이어들에게 무작위로 섞인 카드를 2장씩 나누어 준다. - [x] 분배된 초기 카드를 출력한다. - 딜러는 1장만 공개 diff --git a/src/main/java/domain/BetMoney.java b/src/main/java/domain/BetMoney.java new file mode 100644 index 00000000000..935b5bf3e44 --- /dev/null +++ b/src/main/java/domain/BetMoney.java @@ -0,0 +1,17 @@ +package domain; + +public record BetMoney(int betAmount) { + public BetMoney { + validateIsPositive(betAmount); + } + + private void validateIsPositive(int betAmount) { + if (betAmount <= 0) { + throw new IllegalArgumentException("배팅 금액은 양수여야 합니다."); + } + } + + public int getBetAmount() { + return betAmount; + } +} diff --git a/src/test/java/domain/BetMoneyTest.java b/src/test/java/domain/BetMoneyTest.java new file mode 100644 index 00000000000..1c49bd55ae5 --- /dev/null +++ b/src/test/java/domain/BetMoneyTest.java @@ -0,0 +1,33 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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; + +class BetMoneyTest { + @Test + @DisplayName("양수를 입력 받으면 베팅 금액 객체를 정상적으로 생성한다.") + void shouldReturnBetMoneyForValidBetAmount() { + // given + int betAmount = 1000; + + // when + BetMoney betMoney = new BetMoney(betAmount); + + // then + assertThat(betMoney.getBetAmount()).isEqualTo(betAmount); + } + + @ParameterizedTest + @DisplayName("0 이하의 수를 입력 받으면 예외가 발생한다.") + @ValueSource(ints = {0, -5000}) + void shouldThrowExceptionForInvalidBetAmount(int betAmount) { + // when & then + assertThatThrownBy(() -> new BetMoney(betAmount)) + .isInstanceOf(IllegalArgumentException.class); + } +} From c19e80f2c137925eadacff71ef537934555fb47b Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 01:19:32 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat(InputView):=20=EB=B0=B0=ED=8C=85=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배팅 금액이 공백인 경우 예외 처리 - 숫자가 아닌 문자를 입력한 경우 예외 처리 - 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 --- README.md | 8 +++--- src/main/java/view/InputView.java | 43 +++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d2465f8fa9c..652c4c65c4f 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ - [x] 최대 참여 인원(5명)을 초과하는 경우 예외 처리 - [x] 이름에 한글 또는 영문을 제외한 문자가 포함되는 경우 예외 처리 - [ ] 각 플레이어에게 배팅 금액을 입력받는다. - - [ ] 사용자 입력값 유효성 검사 - - [ ] 배팅 금액이 공백인 경우 예외 처리 - - [ ] 숫자가 아닌 문자를 입력한 경우 예외 처리 - - [ ] 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 + - [x] 사용자 입력값 유효성 검사 + - [x] 배팅 금액이 공백인 경우 예외 처리 + - [x] 숫자가 아닌 문자를 입력한 경우 예외 처리 + - [x] 배팅 금액이 정수 범위를 초과하는 경우 예외 처리 - [x] 도메인 규칙 검증 - [x] 배팅 금액이 0 이하인 경우 예외 처리 - [x] 딜러와 플레이어들에게 무작위로 섞인 카드를 2장씩 나누어 준다. diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 08b718771f9..5dbaca8c379 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -8,16 +8,49 @@ public class InputView { private static final Scanner scanner = new Scanner(System.in); public List readNames() { - String line = scanner.nextLine(); + String line = readLineAndTrim(); validateIsBlank(line); List names = Arrays.stream(line.split("\\s*,\\s*")).toList(); names.forEach(this::validateIsBlank); return names; } + private String readLineAndTrim() { + return scanner.nextLine().trim(); + } + + private void validateIsBlank(String input) { + if (input.isBlank()) { + throw new IllegalArgumentException("공백은 허용되지 않습니다"); + } + } + + public int readBetAmount() { + String line = readLineAndTrim(); + validateIsBlank(line); + validateIsNumeric(line); + return validateAndParseToInt(line); + } + + private void validateIsNumeric(String input) { + if (input.matches("-?\\d+")) { + return; + } + throw new IllegalArgumentException("숫자가 아닌 문자를 입력할 수 없습니다."); + } + + private int validateAndParseToInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("배팅 금액은 정수 범위를 초과할 수 없습니다."); + } + } + public boolean readHitOrStand() { - String input = scanner.nextLine().trim(); + String input = readLineAndTrim(); validateIsBlank(input); + input = input.trim(); if (input.equals("y")) { return true; } @@ -26,10 +59,4 @@ public boolean readHitOrStand() { } throw new IllegalArgumentException("y 혹은 n만 입력 가능합니다."); } - - private void validateIsBlank(String input) { - if (input.isBlank()) { - throw new IllegalArgumentException("공백은 허용되지 않습니다"); - } - } } From 74e7c4501ec4ccebc1447253fc3d119e218279d7 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 02:14:10 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20=EA=B0=81=20=ED=94=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=EA=B2=8C=20=EB=B0=B0=ED=8C=85=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=EC=9D=84=20=EC=9E=85=EB=A0=A5=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컨트롤러에서 플레이어별로 배팅 금액을 입력받아 Player 객체를 생성하는 흐름 구현 - Player 생성 시 미리 검증된 Name과 BetMoney를 필수로 받도록 도메인 생성자 변경 - 도메인 시그니처 변경에 따라 Game, Players 및 연관 테스트 코드 전면 수정 --- README.md | 2 +- .../java/controller/BlackJackController.java | 47 +++++++++++++++++-- src/main/java/domain/Dealer.java | 2 +- src/main/java/domain/Game.java | 3 +- src/main/java/domain/Participant.java | 12 ++--- src/main/java/domain/Player.java | 4 +- src/main/java/domain/Players.java | 24 +++------- src/main/java/view/OutputView.java | 10 +++- src/test/java/domain/GameResultTest.java | 16 +++++-- src/test/java/domain/GameTest.java | 12 ++++- src/test/java/domain/ParticipantTest.java | 5 +- src/test/java/domain/PlayerTest.java | 2 +- src/test/java/domain/PlayersTest.java | 16 ++++++- 13 files changed, 109 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 652c4c65c4f..c65dfb14586 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ - [x] 도메인 규칙 검증 - [x] 최대 참여 인원(5명)을 초과하는 경우 예외 처리 - [x] 이름에 한글 또는 영문을 제외한 문자가 포함되는 경우 예외 처리 -- [ ] 각 플레이어에게 배팅 금액을 입력받는다. +- [x] 각 플레이어에게 배팅 금액을 입력받는다. - [x] 사용자 입력값 유효성 검사 - [x] 배팅 금액이 공백인 경우 예외 처리 - [x] 숫자가 아닌 문자를 입력한 경우 예외 처리 diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index a6a4f8c95fa..535ddb3bbb1 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -1,13 +1,16 @@ package controller; +import domain.BetMoney; import domain.CardShuffleStrategy; import domain.Dealer; import domain.Game; +import domain.Name; import domain.Participant; import domain.Player; import domain.Players; import dto.GameResultDto; import dto.ParticipantDto; +import java.util.ArrayList; import java.util.List; import view.InputView; import view.OutputView; @@ -36,11 +39,21 @@ public void doGame() { } private Game setUpGame() { - List playerNames = repeatAskPlayerNamesUntilSuccess(); - return Game.registerParticipantsAndPrepareTotalDeck(playerNames, strategy); + Players players = generatePlayers(); + return Game.registerParticipantsAndPrepareTotalDeck(players, strategy); } - private List repeatAskPlayerNamesUntilSuccess() { + private Players generatePlayers() { + List playerNames = repeatAskPlayerNamesUntilSuccess(); + List players = new ArrayList<>(); + for (Name playerName : playerNames) { + Player player = generatePlayer(playerName); + players.add(player); + } + return Players.of(players); + } + + private List repeatAskPlayerNamesUntilSuccess() { try { return askPlayerNames(); } catch (IllegalArgumentException e) { @@ -49,9 +62,33 @@ private List repeatAskPlayerNamesUntilSuccess() { } } - private List askPlayerNames() { + private List askPlayerNames() { outputView.printNamePrompt(); - return inputView.readNames(); + List playerNames = inputView.readNames(); + Players.validatePlayersCount(playerNames.size()); + return playerNames.stream() + .map(Name::new) + .toList(); + } + + private Player generatePlayer(Name playerName) { + BetMoney betMoney = repeatAskBetMoneyUntilSuccess(playerName); + return new Player(playerName, betMoney); + } + + private BetMoney repeatAskBetMoneyUntilSuccess(Name playerName) { + try { + return askBetAmount(playerName); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e); + return repeatAskBetMoneyUntilSuccess(playerName); + } + } + + private BetMoney askBetAmount(Name playerName) { + outputView.printBetMoneyPrompt(playerName.name()); + int betAmount = inputView.readBetAmount(); + return new BetMoney(betAmount); } private void drawInitialCardsAndShowResult(Game game) { diff --git a/src/main/java/domain/Dealer.java b/src/main/java/domain/Dealer.java index 56723eab567..f92529b281f 100644 --- a/src/main/java/domain/Dealer.java +++ b/src/main/java/domain/Dealer.java @@ -4,7 +4,7 @@ public class Dealer extends Participant { private static final int MINIMUM_TOTAL_SCORE = 16; - private static final String DEALER_NAME = "딜러"; + private static final Name DEALER_NAME = new Name("딜러"); public Dealer() { super(DEALER_NAME); diff --git a/src/main/java/domain/Game.java b/src/main/java/domain/Game.java index 0c718fcb03d..2d35756dcaa 100644 --- a/src/main/java/domain/Game.java +++ b/src/main/java/domain/Game.java @@ -14,11 +14,10 @@ private Game(Deck totalDeck, Dealer dealer, Players players) { this.players = players; } - public static Game registerParticipantsAndPrepareTotalDeck(List playerNames, + public static Game registerParticipantsAndPrepareTotalDeck(Players players, CardShuffleStrategy strategy) { Deck totalDeck = Deck.createTotalDeckAndShuffle(strategy); Dealer dealer = new Dealer(); - Players players = Players.of(playerNames); return new Game(totalDeck, dealer, players); } diff --git a/src/main/java/domain/Participant.java b/src/main/java/domain/Participant.java index 9776c4958a7..6058824e19b 100644 --- a/src/main/java/domain/Participant.java +++ b/src/main/java/domain/Participant.java @@ -6,8 +6,8 @@ public abstract class Participant { private final Name name; private final Cards cards; - protected Participant(String name) { - this.name = new Name(name); + protected Participant(Name name) { + this.name = name; this.cards = new Cards(); } @@ -27,14 +27,14 @@ public void addCard(Card newCard) { cards.addCard(newCard); } - public int getCardsSum() { - return cards.calculateCardScoreSum(); - } - public String getName() { return name.name(); } + public int getCardsSum() { + return cards.calculateCardScoreSum(); + } + public List getCards() { return cards.getCards(); } diff --git a/src/main/java/domain/Player.java b/src/main/java/domain/Player.java index 33645652df2..9fbe517990e 100644 --- a/src/main/java/domain/Player.java +++ b/src/main/java/domain/Player.java @@ -3,9 +3,11 @@ import java.util.List; public class Player extends Participant { + private final BetMoney betMoney; - public Player(String name) { + public Player(Name name, BetMoney betMoney) { super(name); + this.betMoney = betMoney; } @Override diff --git a/src/main/java/domain/Players.java b/src/main/java/domain/Players.java index acb39a302bd..d60d824ae5c 100644 --- a/src/main/java/domain/Players.java +++ b/src/main/java/domain/Players.java @@ -1,7 +1,5 @@ package domain; -import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -11,32 +9,22 @@ public class Players implements Iterable { private final List players; private Players(List players) { - this.players = players; + validatePlayersCount(players.size()); + this.players = List.copyOf(players); } - public static Players of(List playerNames) { - validatePlayersCount(playerNames); - List players = createPlayers(playerNames); + public static Players of(List players) { return new Players(players); } - private static void validatePlayersCount(List playerNames) { - if (playerNames.size() > MAX_PLAYER_NUMBER) { + public static void validatePlayersCount(int playerCount) { + if (playerCount > MAX_PLAYER_NUMBER) { throw new IllegalArgumentException("플레이 가능한 최대 인원을 초과했습니다."); } } - private static List createPlayers(List playerNames) { - List players = new ArrayList<>(); - for (String playerName : playerNames) { - Player player = new Player(playerName); - players.add(player); - } - return players; - } - @Override public Iterator iterator() { - return Collections.unmodifiableList(players).iterator(); + return players.iterator(); } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index 6f0a840d45d..d21a29de318 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -19,6 +19,11 @@ public void printNamePrompt() { System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); } + public void printBetMoneyPrompt(String playerName) { + System.out.println(); + System.out.printf("%s의 배팅 금액은?\n", playerName); + } + public void printInitialCardShare(List playerDtos) { List playerNames = new ArrayList<>(); for (ParticipantDto participantDto : playerDtos) { @@ -46,20 +51,20 @@ public void printHitOrStandPrompt(ParticipantDto playerDto) { public void printCardShareDetail(ParticipantDto participantDto) { String participantCardInfo = consistParticipantCardInfo(participantDto); System.out.println(participantCardInfo); + System.out.println(); } public void printAdditionalCardForDealerDescription() { System.out.println(); System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다."); - System.out.println(); } public void printCardInfosWithSum(List participantDtos) { + System.out.println(); for (ParticipantDto participantDto : participantDtos) { String participantCardInfoWithSum = consistParticipantCardInfoWithSum(participantDto); System.out.println(participantCardInfoWithSum); } - System.out.println(); } private String consistParticipantCardInfoWithSum(ParticipantDto participantDto) { @@ -82,6 +87,7 @@ private List getCardInfos(ParticipantDto participantDto) { } public void printWinTieLossResult(GameResultDto gameResultDto) { + System.out.println(); System.out.println("## 최종 승패"); String dealerWinTieLossResult = consistDealerWinTieLossResult(gameResultDto.dealerWinTieLossResult()); diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java index 2e8029a9257..f2ade6b2ada 100644 --- a/src/test/java/domain/GameResultTest.java +++ b/src/test/java/domain/GameResultTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -13,6 +14,15 @@ class GameResultTest { + private Players createDummyPlayer(List playerNames) { + List players = new ArrayList<>(); + for (String playerName : playerNames) { + Player player = new Player(new Name(playerName), new BetMoney(1000)); + players.add(player); + } + return Players.of(players); + } + private void addCardsToParticipantDeck(Participant participant, Card... cards) { for (Card card : cards) { participant.addCard(card); @@ -30,7 +40,7 @@ void shouldReturnWinTieLossResult() { ); List playerNames = List.of("pobi", "terry", "rati", "gump"); - Players players = Players.of(playerNames); + Players players = createDummyPlayer(playerNames); Iterator playerIterator = players.iterator(); Player player1 = playerIterator.next(); @@ -86,7 +96,7 @@ void shouldThrowExceptionWhenDealerScoreUnderMinimum() { new Card(CardShape.CLOVER, CardContents.THREE) ); List playerNames = List.of("pobi", "terry"); - Players players = Players.of(playerNames); + Players players = createDummyPlayer(playerNames); // when & then assertThatThrownBy(() -> GameResult.calculate(dealer, players)) @@ -103,7 +113,7 @@ void shouldThrowExceptionWhenDealerScoreEqualsMinimum() { new Card(CardShape.CLOVER, CardContents.SIX) ); List playerNames = List.of("pobi", "terry"); - Players players = Players.of(playerNames); + Players players = createDummyPlayer(playerNames); // when & then assertThatThrownBy(() -> GameResult.calculate(dealer, players)) diff --git a/src/test/java/domain/GameTest.java b/src/test/java/domain/GameTest.java index e024f1c7cad..b2d5b8e1849 100644 --- a/src/test/java/domain/GameTest.java +++ b/src/test/java/domain/GameTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,6 +15,15 @@ class GameTest { }; private Game game; + private Players createDummyPlayer() { + List players = new ArrayList<>(); + for (String playerName : PLAYER_NAMES) { + Player player = new Player(new Name(playerName), new BetMoney(1000)); + players.add(player); + } + return Players.of(players); + } + private void addCardsToPlayerDeck(Participant participant, Card... cards) { for (Card card : cards) { participant.addCard(card); @@ -22,7 +32,7 @@ private void addCardsToPlayerDeck(Participant participant, Card... cards) { @BeforeEach void setUp() { - game = Game.registerParticipantsAndPrepareTotalDeck(PLAYER_NAMES, FIXED_SHUFFLE_STRATEGY); + game = Game.registerParticipantsAndPrepareTotalDeck(createDummyPlayer(), FIXED_SHUFFLE_STRATEGY); } @Test diff --git a/src/test/java/domain/ParticipantTest.java b/src/test/java/domain/ParticipantTest.java index 1de3a50e2a8..a1eade98e65 100644 --- a/src/test/java/domain/ParticipantTest.java +++ b/src/test/java/domain/ParticipantTest.java @@ -11,8 +11,7 @@ class ParticipantTest { @DisplayName("참가자가 카드를 뽑으면, 참가자의 덱에 카드가 추가된다.") void shouldAddCardToParticipantDeck() { // given - String name = "테스트플레이어"; - TestParticipant testParticipant = new TestParticipant(name); + TestParticipant testParticipant = new TestParticipant(new Name("참가자")); Card card = new Card(CardShape.SPADE, CardContents.A); // when @@ -23,7 +22,7 @@ void shouldAddCardToParticipantDeck() { } private static class TestParticipant extends Participant { - public TestParticipant(String name) { + protected TestParticipant(Name name) { super(name); } diff --git a/src/test/java/domain/PlayerTest.java b/src/test/java/domain/PlayerTest.java index beb850a4c72..ef1608fcda3 100644 --- a/src/test/java/domain/PlayerTest.java +++ b/src/test/java/domain/PlayerTest.java @@ -49,7 +49,7 @@ void shouldReturnFalseWhenBust() { } private Player createPlayerWithCards(Card... cards) { - Player player = new Player("pobi"); + Player player = new Player(new Name("pobi"), new BetMoney(1000)); for (Card card : cards) { player.addCard(card); } diff --git a/src/test/java/domain/PlayersTest.java b/src/test/java/domain/PlayersTest.java index 68f45bbdb2b..3cdde8aa0d9 100644 --- a/src/test/java/domain/PlayersTest.java +++ b/src/test/java/domain/PlayersTest.java @@ -3,20 +3,31 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class PlayersTest { + private List createDummyPlayers(List playerNames) { + List players = new ArrayList<>(); + for (String playerName : playerNames) { + Player player = new Player(new Name(playerName), new BetMoney(1000)); + players.add(player); + } + return players; + } + @Test @DisplayName("플레이어가 5명 이하면 정상적으로 Players 객체를 생성한다.") void shouldReturnPlayersWhenPlayerNumberIsMaximumOrLess() { // given List testPlayerNames = List.of("pobi", "terry", "rati", "gump", "junny"); + List testPlayers = createDummyPlayers(testPlayerNames); // when & then assertDoesNotThrow( - () -> Players.of(testPlayerNames) + () -> Players.of(testPlayers) ); } @@ -25,9 +36,10 @@ void shouldReturnPlayersWhenPlayerNumberIsMaximumOrLess() { void shouldThrowExceptionWhenPlayerNumberOverMaximum() { // given List testPlayerNames = List.of("pobi", "terry", "rati", "gump", "junny", "aron"); + List testPlayers = createDummyPlayers(testPlayerNames); // when & then - assertThatThrownBy(() -> Players.of(testPlayerNames)) + assertThatThrownBy(() -> Players.of(testPlayers)) .isInstanceOf(IllegalArgumentException.class); } } From 9949fac1cb365dffbd26a05bd21c17bcae80ca91 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 03:43:25 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=8A=B9?= =?UTF-8?q?=ED=8C=A8=20=EA=B2=B0=EA=B3=BC=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EC=88=98=EC=9D=B5=20=EA=B3=84=EC=82=B0,?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 블랙잭 상황 제외, 일반적인 승/무/패 상황의 배당률(1, 0, -1)을 적용한 플레이어 수익 계산 로직 구현 - 플레이어들의 수익 합산 결과를 바탕으로 딜러의 최종 수익을 계산하는 로직 추가 - 게임 종료 후 딜러와 플레이어의 최종 수익을 화면에 출력하는 로직 추가 --- README.md | 16 +-- src/main/java/domain/GameResult.java | 49 +++----- src/main/java/domain/Player.java | 4 + src/main/java/domain/PlayerResult.java | 35 ++++++ src/main/java/domain/Result.java | 52 -------- src/main/java/dto/GameResultDto.java | 27 ++-- src/main/java/view/OutputView.java | 21 ++-- src/test/java/domain/GameResultTest.java | 18 ++- src/test/java/domain/GameTest.java | 6 +- src/test/java/domain/PlayerResultTest.java | 136 +++++++++++++++++++++ src/test/java/domain/ResultTest.java | 113 ----------------- 11 files changed, 229 insertions(+), 248 deletions(-) create mode 100644 src/main/java/domain/PlayerResult.java delete mode 100644 src/main/java/domain/Result.java create mode 100644 src/test/java/domain/PlayerResultTest.java delete mode 100644 src/test/java/domain/ResultTest.java diff --git a/README.md b/README.md index c65dfb14586..922c7d9b67a 100644 --- a/README.md +++ b/README.md @@ -65,24 +65,24 @@ - [x] Ace가 2장 이상 포함된 경우 - 2장 이상을 11로 계산하면 무조건 22점 이상으로 Bust가 나므로, 11로 계산할 수 있는 Ace는 최대 1장 - [x] 1장의 Ace를 11로 계산했을 때 Bust가 난다면, 모든 Ace를 1점으로 계산 -- [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 최종 수익을 계산한다. +- [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 플레이어의 최종 수익을 계산한다. - [ ] 플레이어가 승리한 경우 - [ ] 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 - - [ ] 딜러만 Bust인 경우: 배팅 금액의 1배 수익 - - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 + - [x] 딜러만 Bust인 경우: 배팅 금액의 1배 수익 + - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 - [ ] 플레이어가 패배한 경우: 배팅 금액만큼 손실 (-1배) - [ ] 딜러만 Blackjack인 경우 - - [ ] 플레이어가 Bust인 경우 - - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 + - [x] 플레이어가 Bust인 경우 + - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 - [ ] 무승부인 경우: 수익 0 - [ ] 플레이어와 딜러 모두 Blackjack인 경우 - - [ ] 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 -- [ ] 딜러의 수익은 플레이어들의 최종 수익 합의 반대로 계산한다. + - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 +- [x] 딜러의 수익은 플레이어들의 최종 수익 합의 반대로 계산한다. ### 4. 게임 결과 출력 - [x] 모든 참가자의 최종 카드 목록과 점수를 출력한다. -- [ ] 딜러와 플레이어들의 최종 수익을 출력한다. +- [x] 딜러와 플레이어들의 최종 수익을 출력한다. ## 🗂️ 클래스 다이어그램 diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java index 9bb5d5f946e..df74b491b74 100644 --- a/src/main/java/domain/GameResult.java +++ b/src/main/java/domain/GameResult.java @@ -1,22 +1,21 @@ package domain; -import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; public class GameResult { - private final Map playerResults; - private final Map dealerResult; + private final Map playerResults; + private final long dealerResult; - private GameResult(Map playerResults, Map dealerResult) { + private GameResult(Map playerResults, long dealerResult) { this.playerResults = playerResults; this.dealerResult = dealerResult; } public static GameResult calculate(Dealer dealer, Players players) { validateCalculatable(dealer); - Map playerResults = calculatePlayerResults(dealer, players); - Map dealerResult = calculateDealerResult(playerResults); + Map playerResults = calculatePlayerResults(dealer, players); + long dealerResult = calculateDealerResult(playerResults); return new GameResult(playerResults, dealerResult); } @@ -26,40 +25,30 @@ private static void validateCalculatable(Dealer dealer) { } } - private static Map calculatePlayerResults(Dealer dealer, Players players) { - boolean isDealerBust = dealer.isBust(); - int dealerScore = dealer.getCardsSum(); - Map playerWinTieLossResults = new LinkedHashMap<>(); + private static Map calculatePlayerResults(Dealer dealer, Players players) { + Map playerWinTieLossResults = new LinkedHashMap<>(); for (Player player : players) { - Result playerResult = calculatePlayerResult(isDealerBust, dealerScore, player); - playerWinTieLossResults.put(player, playerResult); + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + double rate = playerResult.getReturnRate(); + long profit = player.calculateBettingProfit(rate); + playerWinTieLossResults.put(player, profit); } return playerWinTieLossResults; } - private static Result calculatePlayerResult(boolean isDealerBust, int dealerScore, Player player) { - return Result.determinePlayerResult( - isDealerBust, - player.isBust(), - dealerScore, - player.getCardsSum() - ); - } - - private static Map calculateDealerResult(Map playerResults) { - Map dealerResult = new EnumMap<>(Result.class); - for (Result playerResult : playerResults.values()) { - Result reversed = playerResult.reverse(); - dealerResult.put(reversed, dealerResult.getOrDefault(reversed, 0) + 1); + private static long calculateDealerResult(Map playerResults) { + long playerBettingProfits = 0; + for (long playerResult : playerResults.values()) { + playerBettingProfits += playerResult; } - return dealerResult; + return playerBettingProfits * (-1); } - public Map getPlayerResults() { + public Map getPlayerResults() { return Map.copyOf(playerResults); } - public Map getDealerResult() { - return Map.copyOf(dealerResult); + public long getDealerResult() { + return dealerResult; } } diff --git a/src/main/java/domain/Player.java b/src/main/java/domain/Player.java index 9fbe517990e..fdfdb1c5ed1 100644 --- a/src/main/java/domain/Player.java +++ b/src/main/java/domain/Player.java @@ -19,4 +19,8 @@ public List getInitialVisibleCards() { public boolean isDrawable() { return super.isLessThanMaxScore(); } + + public Long calculateBettingProfit(double returnRate) { + return (long) (betMoney.betAmount() * returnRate); + } } diff --git a/src/main/java/domain/PlayerResult.java b/src/main/java/domain/PlayerResult.java new file mode 100644 index 00000000000..7b24df1a8d8 --- /dev/null +++ b/src/main/java/domain/PlayerResult.java @@ -0,0 +1,35 @@ +package domain; + +public enum PlayerResult { + BLACKJACK(1.5), WIN(1), TIE(0), LOSS(-1); + + private final double returnRate; + + PlayerResult(double returnRate) { + this.returnRate = returnRate; + } + + public static PlayerResult determinePlayerResult(Dealer dealer, Player player) { + if (player.isBust()) { + return PlayerResult.LOSS; + } + if (dealer.isBust()) { + return PlayerResult.WIN; + } + return comparePlayerScoreWithDealerScore(dealer.getCardsSum(), player.getCardsSum()); + } + + private static PlayerResult comparePlayerScoreWithDealerScore(int dealerScore, int playerScore) { + if (dealerScore > playerScore) { + return PlayerResult.LOSS; + } + if (dealerScore == playerScore) { + return PlayerResult.TIE; + } + return PlayerResult.WIN; + } + + public double getReturnRate() { + return returnRate; + } +} diff --git a/src/main/java/domain/Result.java b/src/main/java/domain/Result.java deleted file mode 100644 index ebd40134174..00000000000 --- a/src/main/java/domain/Result.java +++ /dev/null @@ -1,52 +0,0 @@ -package domain; - -public enum Result { - WIN("승"), TIE("무"), LOSS("패"); - - private final String name; - - Result(String name) { - this.name = name; - } - - public static Result determinePlayerResult( - boolean isDealerBust, - boolean isPlayerBust, - int dealerScore, - int playerScore - ) { - if (isPlayerBust) { - return Result.LOSS; - } - if (isDealerBust) { - return Result.WIN; - } - return comparePlayerScoreWithDealerScore(dealerScore, playerScore); - } - - private static Result comparePlayerScoreWithDealerScore(int dealerScore, int playerScore) { - if (dealerScore > playerScore) { - return Result.LOSS; - } - if (dealerScore == playerScore) { - return Result.TIE; - } - return Result.WIN; - } - - public String getName() { - return name; - } - - public Result reverse() { - if (this == Result.WIN) { - return Result.LOSS; - } - - if (this == Result.LOSS) { - return Result.WIN; - } - - return Result.TIE; - } -} diff --git a/src/main/java/dto/GameResultDto.java b/src/main/java/dto/GameResultDto.java index b6fbfffe30c..f1e3c40dfe8 100644 --- a/src/main/java/dto/GameResultDto.java +++ b/src/main/java/dto/GameResultDto.java @@ -2,32 +2,23 @@ import domain.GameResult; import domain.Player; -import domain.Result; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; -public record GameResultDto(Map dealerWinTieLossResult, - Map playerWinTieLossResults) { +public record GameResultDto(Map playerWinTieLossResults, + long dealerWinTieLossResult) { public static GameResultDto from(GameResult gameResult) { - Map dealerResults = convertDealerResult(gameResult.getDealerResult()); - Map playerResults = convertPlayerResult(gameResult.getPlayerResults()); - return new GameResultDto(dealerResults, playerResults); + Map playerResults = convertPlayerResult(gameResult.getPlayerResults()); + long dealerResults = gameResult.getDealerResult(); + return new GameResultDto(playerResults, dealerResults); } - private static Map convertDealerResult(Map dealerWinTieLossResults) { - Map dealerResults = new LinkedHashMap<>(); - for (Entry entry : dealerWinTieLossResults.entrySet()) { - dealerResults.put(entry.getKey().getName(), entry.getValue()); - } - return dealerResults; - } - - private static Map convertPlayerResult(Map playerWinTieLossResults) { - Map playerResults = new LinkedHashMap<>(); - for (Entry entry : playerWinTieLossResults.entrySet()) { - playerResults.put(entry.getKey().getName(), entry.getValue().getName()); + private static Map convertPlayerResult(Map playerWinTieLossResults) { + Map playerResults = new LinkedHashMap<>(); + for (Entry entry : playerWinTieLossResults.entrySet()) { + playerResults.put(entry.getKey().getName(), entry.getValue()); } return playerResults; } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index d21a29de318..d35771d1c17 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -88,7 +88,7 @@ private List getCardInfos(ParticipantDto participantDto) { public void printWinTieLossResult(GameResultDto gameResultDto) { System.out.println(); - System.out.println("## 최종 승패"); + System.out.println("## 최종 수익"); String dealerWinTieLossResult = consistDealerWinTieLossResult(gameResultDto.dealerWinTieLossResult()); System.out.println(dealerWinTieLossResult); @@ -99,24 +99,17 @@ public void printWinTieLossResult(GameResultDto gameResultDto) { } } - private String consistDealerWinTieLossResult(Map dealerWinTieLossResult) { - List dealerResult = new ArrayList<>(); - for (Entry result : dealerWinTieLossResult.entrySet()) { - int count = result.getValue(); - String winTieLoss = result.getKey(); - String resultInFormat = String.format("%d%s", count, winTieLoss); - dealerResult.add(resultInFormat); - } - return String.format("딜러: %s", String.join(" ", dealerResult)); + private String consistDealerWinTieLossResult(Long dealerWinTieLossResult) { + return String.format("딜러: %d", dealerWinTieLossResult); } private List consistPlayerWinTieLossResults(GameResultDto gameResultDto) { List playerResult = new ArrayList<>(); - Map playerWinLossResults = gameResultDto.playerWinTieLossResults(); - for (Entry result : playerWinLossResults.entrySet()) { + Map playerWinLossResults = gameResultDto.playerWinTieLossResults(); + for (Entry result : playerWinLossResults.entrySet()) { String name = result.getKey(); - String winTieLoss = result.getValue(); - String resultInFormat = String.format("%s: %s", name, winTieLoss); + Long winTieLoss = result.getValue(); + String resultInFormat = String.format("%s: %d", name, winTieLoss); playerResult.add(resultInFormat); } return playerResult; diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java index f2ade6b2ada..8c2c1f95563 100644 --- a/src/test/java/domain/GameResultTest.java +++ b/src/test/java/domain/GameResultTest.java @@ -64,20 +64,18 @@ void shouldReturnWinTieLossResult() { new Card(CardShape.HEART, CardContents.NINE) ); - Map expectPlayerWinLossResults = new LinkedHashMap<>(); - expectPlayerWinLossResults.put(player1, Result.LOSS); - expectPlayerWinLossResults.put(player2, Result.LOSS); - expectPlayerWinLossResults.put(player3, Result.WIN); - expectPlayerWinLossResults.put(player4, Result.WIN); + Map expectPlayerWinLossResults = new LinkedHashMap<>(); + expectPlayerWinLossResults.put(player1, -1000L); + expectPlayerWinLossResults.put(player2, -1000L); + expectPlayerWinLossResults.put(player3, 1000L); + expectPlayerWinLossResults.put(player4, 1000L); - Map expectDealerWinLossResults = new LinkedHashMap<>(); - expectDealerWinLossResults.put(Result.WIN, 2); - expectDealerWinLossResults.put(Result.LOSS, 2); + long expectDealerWinLossResults = 0L; // when GameResult gameResult = GameResult.calculate(dealer, players); - Map playerResults = gameResult.getPlayerResults(); - Map dealerResults = gameResult.getDealerResult(); + Map playerResults = gameResult.getPlayerResults(); + long dealerResults = gameResult.getDealerResult(); // then assertThat(playerResults).isEqualTo(expectPlayerWinLossResults); diff --git a/src/test/java/domain/GameTest.java b/src/test/java/domain/GameTest.java index b2d5b8e1849..78a868443ae 100644 --- a/src/test/java/domain/GameTest.java +++ b/src/test/java/domain/GameTest.java @@ -24,7 +24,7 @@ private Players createDummyPlayer() { return Players.of(players); } - private void addCardsToPlayerDeck(Participant participant, Card... cards) { + private void addCardsToParticipantDeck(Participant participant, Card... cards) { for (Card card : cards) { participant.addCard(card); } @@ -61,13 +61,13 @@ void shouldReadyParticipantDecksWithTwoInitialCards() { void shouldReturnTrueWhenParticipantsCanDrawCardUnderCondition() { // given Dealer dealer = game.getDealer(); - addCardsToPlayerDeck(dealer, + addCardsToParticipantDeck(dealer, new Card(CardShape.SPADE, CardContents.TWO), new Card(CardShape.CLOVER, CardContents.THREE) ); Player player = game.getPlayers().iterator().next(); - addCardsToPlayerDeck(player, + addCardsToParticipantDeck(player, new Card(CardShape.HEART, CardContents.TWO), new Card(CardShape.HEART, CardContents.THREE) ); diff --git a/src/test/java/domain/PlayerResultTest.java b/src/test/java/domain/PlayerResultTest.java new file mode 100644 index 00000000000..93ad9a21fe1 --- /dev/null +++ b/src/test/java/domain/PlayerResultTest.java @@ -0,0 +1,136 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PlayerResultTest { + private void addCardsToParticipantDeck(Participant participant, Card... cards) { + for (Card card : cards) { + participant.addCard(card); + } + } + + @Nested + class DeterminePlayerResultTest { + @Test + @DisplayName("플레이어가 버스트라면 딜러의 버스트 여부와 상관없이 무조건 패배한다.") + void shouldReturnLossWhenPlayerIsBust() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.THREE), + new Card(CardShape.HEART, CardContents.TEN) + ); + + Player player = new Player(new Name("플레이어"), new BetMoney(1000)); + addCardsToParticipantDeck(player, + new Card(CardShape.DIAMOND, CardContents.TEN), + new Card(CardShape.HEART, CardContents.THREE), + new Card(CardShape.SPADE, CardContents.TEN) + ); + + // when + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + + // then + assertThat(playerResult).isEqualTo(PlayerResult.LOSS); + } + + @Test + @DisplayName("플레이어는 버스트가 아니고 딜러만 버스트인 경우 승리를 반환한다.") + void shouldReturnWinWhenPlayerIsNotBustButDealerIsBust() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.THREE), + new Card(CardShape.HEART, CardContents.TEN) + ); + + Player player = new Player(new Name("플레이어"), new BetMoney(1000)); + addCardsToParticipantDeck(player, + new Card(CardShape.DIAMOND, CardContents.TEN), + new Card(CardShape.HEART, CardContents.THREE) + ); + + // when + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + + // then + assertThat(playerResult).isEqualTo(PlayerResult.WIN); + } + + @Test + @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수가 딜러 점수보다 높으면 승리를 반환한다.") + void shouldReturnWinWhenNeitherIsBustAndPlayerScoreIsHigherThanDealerScore() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.SEVEN) + ); + + Player player = new Player(new Name("플레이어"), new BetMoney(1000)); + addCardsToParticipantDeck(player, + new Card(CardShape.DIAMOND, CardContents.TEN), + new Card(CardShape.HEART, CardContents.EIGHT) + ); + + // when + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + + // then + assertThat(playerResult).isEqualTo(PlayerResult.WIN); + } + + @Test + @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수가 딜러 점수보다 낮으면 패배를 반환한다.") + void shouldReturnLossWhenNeitherIsBustAndPlayerScoreIsLowerThanDealerScore() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.TEN) + ); + + Player player = new Player(new Name("플레이어"), new BetMoney(1000)); + addCardsToParticipantDeck(player, + new Card(CardShape.DIAMOND, CardContents.TEN), + new Card(CardShape.HEART, CardContents.NINE) + ); + + // when + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + + // then + assertThat(playerResult).isEqualTo(PlayerResult.LOSS); + } + + @Test + @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수와 딜러 점수가 같다면 무승부를 반환한다.") + void shouldReturnTieWhenNeitherIsBustAndScoresAreEqual() { + // given + Dealer dealer = new Dealer(); + addCardsToParticipantDeck(dealer, + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.CLOVER, CardContents.SEVEN) + ); + + Player player = new Player(new Name("플레이어"), new BetMoney(1000)); + addCardsToParticipantDeck(player, + new Card(CardShape.DIAMOND, CardContents.TEN), + new Card(CardShape.HEART, CardContents.SEVEN) + ); + + // when + PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); + + // then + assertThat(playerResult).isEqualTo(PlayerResult.TIE); + } + } +} diff --git a/src/test/java/domain/ResultTest.java b/src/test/java/domain/ResultTest.java deleted file mode 100644 index fc49e7c67f8..00000000000 --- a/src/test/java/domain/ResultTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package domain; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class ResultTest { - @Nested - class DeterminePlayerResultTest { - @Test - @DisplayName("플레이어가 버스트라면 딜러의 버스트 여부와 상관없이 무조건 패배한다.") - void shouldReturnLossWhenPlayerIsBust() { - // given - boolean isDealerBust = true; - boolean isPlayerBust = true; - int dealerScore = 23; - int playerScore = 22; - - // when - Result result = Result.determinePlayerResult(isDealerBust, isPlayerBust, dealerScore, playerScore); - - // then - assertThat(result).isEqualTo(Result.LOSS); - } - - @Test - @DisplayName("플레이어는 버스트가 아니고 딜러만 버스트인 경우 승리를 반환한다.") - void shouldReturnWinWhenPlayerIsNotBustButDealerIsBust() { - // given - boolean isDealerBust = true; - boolean isPlayerBust = false; - int dealerScore = 23; - int playerScore = 15; - - // when - Result result = Result.determinePlayerResult(isDealerBust, isPlayerBust, dealerScore, playerScore); - - // then - assertThat(result).isEqualTo(Result.WIN); - } - - @Test - @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수가 딜러 점수보다 높으면 승리를 반환한다.") - void shouldReturnWinWhenNeitherIsBustAndPlayerScoreIsHigherThanDealerScore() { - // given - boolean isDealerBust = false; - boolean isPlayerBust = false; - int dealerScore = 17; - int playerScore = 20; - - // when - Result result = Result.determinePlayerResult(isDealerBust, isPlayerBust, dealerScore, playerScore); - - // then - assertThat(result).isEqualTo(Result.WIN); - } - - @Test - @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수가 딜러 점수보다 낮으면 패배를 반환한다.") - void shouldReturnLossWhenNeitherIsBustAndPlayerScoreIsLowerThanDealerScore() { - // given - boolean isDealerBust = false; - boolean isPlayerBust = false; - int dealerScore = 20; - int playerScore = 15; - - // when - Result result = Result.determinePlayerResult(isDealerBust, isPlayerBust, dealerScore, playerScore); - - // then - assertThat(result).isEqualTo(Result.LOSS); - } - - @Test - @DisplayName("플레이어와 딜러 모두 버스트가 아니고, 플레이어 점수와 딜러 점수가 같다면 무승부를 반환한다.") - void shouldReturnTieWhenNeitherIsBustAndScoresAreEqual() { - // given - boolean isDealerBust = false; - boolean isPlayerBust = false; - int dealerScore = 20; - int playerScore = 20; - - // when - Result result = Result.determinePlayerResult(isDealerBust, isPlayerBust, dealerScore, playerScore); - - // then - assertThat(result).isEqualTo(Result.TIE); - } - } - - @Nested - class ReverseTest { - @Test - @DisplayName("승리를 뒤집으면 패배가 된다.") - void shouldReturnLossWhenReversingWin() { - assertThat(Result.WIN.reverse()).isEqualTo(Result.LOSS); - } - - @Test - @DisplayName("패배를 뒤집으면 승리가 된다.") - void shouldReturnWinWhenReversingLoss() { - assertThat(Result.LOSS.reverse()).isEqualTo(Result.WIN); - } - - @Test - @DisplayName("무승부를 뒤집으면 그대로 무승부가 된다.") - void shouldReturnTieWhenReversingTie() { - assertThat(Result.TIE.reverse()).isEqualTo(Result.TIE); - } - } -} From 001e3a390470102c1210b23a5619b5eb86d42b91 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 04:14:25 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=ED=8A=B9=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4(=EB=B8=94=EB=9E=99=EC=9E=AD)=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=B5=9C=EC=A2=85=20=EC=88=98=EC=9D=B5=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 처음 받은 2장의 카드가 21점일 경우(블랙잭) 배팅 금액의 1.5배 수익을 반환하는 로직 추가 --- README.md | 14 +-- src/main/java/domain/Cards.java | 8 +- src/main/java/domain/Participant.java | 4 + src/main/java/domain/PlayerResult.java | 3 + src/test/java/domain/CardsTest.java | 104 +++++++++++++++-------- src/test/java/domain/DealerTest.java | 2 +- src/test/java/domain/GameResultTest.java | 6 +- 7 files changed, 89 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 922c7d9b67a..dc20f594b1c 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,17 @@ - [x] Ace가 2장 이상 포함된 경우 - 2장 이상을 11로 계산하면 무조건 22점 이상으로 Bust가 나므로, 11로 계산할 수 있는 Ace는 최대 1장 - [x] 1장의 Ace를 11로 계산했을 때 Bust가 난다면, 모든 Ace를 1점으로 계산 -- [ ] 딜러와 플레이어의 최종 승패 및 조건에 따른 플레이어의 최종 수익을 계산한다. - - [ ] 플레이어가 승리한 경우 - - [ ] 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 +- [x] 딜러와 플레이어의 최종 승패 및 조건에 따른 플레이어의 최종 수익을 계산한다. + - [x] 플레이어가 승리한 경우 + - [x] 플레이어만 Blackjack인 경우: 배팅 금액의 1.5배 수익 - [x] 딜러만 Bust인 경우: 배팅 금액의 1배 수익 - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 높은 경우: 배팅 금액의 1배 수익 - - [ ] 플레이어가 패배한 경우: 배팅 금액만큼 손실 (-1배) - - [ ] 딜러만 Blackjack인 경우 + - [x] 플레이어가 패배한 경우: 배팅 금액만큼 손실 (-1배) - [x] 플레이어가 Bust인 경우 + - [x] 딜러만 Blackjack인 경우 - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어의 점수가 딜러의 점수보다 낮은 경우 - - [ ] 무승부인 경우: 수익 0 - - [ ] 플레이어와 딜러 모두 Blackjack인 경우 + - [x] 무승부인 경우: 수익 0 + - [x] 플레이어와 딜러 모두 Blackjack인 경우 - [x] 플레이어와 딜러 모두 Bust가 아니고, 플레이어와 딜러의 점수가 동일한 경우 - [x] 딜러의 수익은 플레이어들의 최종 수익 합의 반대로 계산한다. diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index 5f3d8d7ff4e..ca708a46316 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -6,6 +6,7 @@ public class Cards { private static final int BUST_CRITERIA = 21; private static final int ACE_EXTRA_SCORE = 10; + private static final int BLACKJACK_CARD_SIZE = 2; private final List cards; @@ -55,10 +56,9 @@ public boolean isBust() { return calculateCardScoreSum() > BUST_CRITERIA; } -// @Override -// public Iterator iterator() { -// return List.copyOf(cards).iterator(); -// } + public boolean isBlackJack() { + return cards.size() == BLACKJACK_CARD_SIZE && calculateCardScoreSum() == BUST_CRITERIA; + } public List getCards() { return List.copyOf(cards); diff --git a/src/main/java/domain/Participant.java b/src/main/java/domain/Participant.java index 6058824e19b..9ef10e6b7f5 100644 --- a/src/main/java/domain/Participant.java +++ b/src/main/java/domain/Participant.java @@ -27,6 +27,10 @@ public void addCard(Card newCard) { cards.addCard(newCard); } + public boolean isBlackJack() { + return cards.isBlackJack(); + } + public String getName() { return name.name(); } diff --git a/src/main/java/domain/PlayerResult.java b/src/main/java/domain/PlayerResult.java index 7b24df1a8d8..60015a507f8 100644 --- a/src/main/java/domain/PlayerResult.java +++ b/src/main/java/domain/PlayerResult.java @@ -13,6 +13,9 @@ public static PlayerResult determinePlayerResult(Dealer dealer, Player player) { if (player.isBust()) { return PlayerResult.LOSS; } + if (player.isBlackJack() && !dealer.isBlackJack()) { + return PlayerResult.BLACKJACK; + } if (dealer.isBust()) { return PlayerResult.WIN; } diff --git a/src/test/java/domain/CardsTest.java b/src/test/java/domain/CardsTest.java index ad6fab9f9ae..a3b515c4619 100644 --- a/src/test/java/domain/CardsTest.java +++ b/src/test/java/domain/CardsTest.java @@ -84,6 +84,64 @@ void shouldThrowExceptionForDuplicatedCard() { } } + @Nested + class CalculateCardScoreSumTest { + @Test + @DisplayName("Ace가 없는 경우 카드 점수의 합을 정확히 계산한다.") + void shouldReturnScoreSumWithoutAce() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN) + ); + + // when & then + assertThat(cards.calculateCardScoreSum()).isEqualTo(20); + } + + @Test + @DisplayName("Ace가 포함되어 있고 11점으로 계산해도 버스트가 나지 않는다면, Ace를 11점으로 계산한다.") + void shouldReturnScoreSumWithAceCalculatedAsElevenWhenNotBust() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.A) + ); + + // when & then + assertThat(cards.calculateCardScoreSum()).isEqualTo(21); + } + + @Test + @DisplayName("Ace가 포함되어 있으나 11점으로 계산 시 버스트가 난다면, Ace를 1점으로 계산한다.") + void shouldReturnScoreSumWithAceCalculatedAsOneWhenElevenCausesBust() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.TEN), + new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.HEART, CardContents.A) + ); + + // when & then + assertThat(cards.calculateCardScoreSum()).isEqualTo(21); + } + + @Test + @DisplayName("Ace가 2장 이상일 때, 최대 1장만 11점으로 계산하고 나머지는 1점으로 계산한다.") + void shouldReturnScoreSumWithMultiplyAces() { + // given + Cards cards = createCardsWithCards( + new Card(CardShape.SPADE, CardContents.A), + new Card(CardShape.HEART, CardContents.A), + new Card(CardShape.CLOVER, CardContents.A), + new Card(CardShape.DIAMOND, CardContents.TEN) + ); + + // when & then + assertThat(cards.calculateCardScoreSum()).isEqualTo(13); + } + } + @Nested class IsLessThanMaxScoreTest { @Test @@ -160,23 +218,10 @@ void shouldReturnFalseWhenDeckSumEqualsMaximumOrLess() { } @Nested - class CalculateCardScoreSumTest { + class IsBlackJackTest { @Test - @DisplayName("Ace가 없는 경우 카드 점수의 합을 정확히 계산한다.") - void shouldReturnScoreSumWithoutAce() { - // given - Cards cards = createCardsWithCards( - new Card(CardShape.SPADE, CardContents.TEN), - new Card(CardShape.HEART, CardContents.TEN) - ); - - // when & then - assertThat(cards.calculateCardScoreSum()).isEqualTo(20); - } - - @Test - @DisplayName("Ace가 포함되어 있고 11점으로 계산해도 버스트가 나지 않는다면, Ace를 11점으로 계산한다.") - void shouldReturnScoreSumWithAceCalculatedAsElevenWhenNotBust() { + @DisplayName("카드가 두장이고, 카드의 합이 21일 때만 블랙잭으로 판정한다.") + void shouldReturnTrueWhenTwoCardsSumEqualsTwentyOne() { // given Cards cards = createCardsWithCards( new Card(CardShape.SPADE, CardContents.TEN), @@ -184,36 +229,21 @@ void shouldReturnScoreSumWithAceCalculatedAsElevenWhenNotBust() { ); // when & then - assertThat(cards.calculateCardScoreSum()).isEqualTo(21); + assertTrue(cards.isBlackJack()); } @Test - @DisplayName("Ace가 포함되어 있으나 11점으로 계산 시 버스트가 난다면, Ace를 1점으로 계산한다.") - void shouldReturnScoreSumWithAceCalculatedAsOneWhenElevenCausesBust() { + @DisplayName("카드가 두장 이상이고, 카드의 합이 21이라면 블랙잭으로 판정하지 않는다.") + void shouldReturnTrueWhenMoreThanTwoCardsSumEqualsTwentyOne() { // given Cards cards = createCardsWithCards( - new Card(CardShape.SPADE, CardContents.TEN), - new Card(CardShape.HEART, CardContents.TEN), + new Card(CardShape.SPADE, CardContents.FIVE), + new Card(CardShape.HEART, CardContents.FIVE), new Card(CardShape.HEART, CardContents.A) ); // when & then - assertThat(cards.calculateCardScoreSum()).isEqualTo(21); - } - - @Test - @DisplayName("Ace가 2장 이상일 때, 최대 1장만 11점으로 계산하고 나머지는 1점으로 계산한다.") - void shouldReturnScoreSumWithMultiplyAces() { - // given - Cards cards = createCardsWithCards( - new Card(CardShape.SPADE, CardContents.A), - new Card(CardShape.HEART, CardContents.A), - new Card(CardShape.CLOVER, CardContents.A), - new Card(CardShape.DIAMOND, CardContents.TEN) - ); - - // when & then - assertThat(cards.calculateCardScoreSum()).isEqualTo(13); + assertFalse(cards.isBlackJack()); } } } diff --git a/src/test/java/domain/DealerTest.java b/src/test/java/domain/DealerTest.java index 36fa570cfb0..75dcbc18dbc 100644 --- a/src/test/java/domain/DealerTest.java +++ b/src/test/java/domain/DealerTest.java @@ -116,7 +116,7 @@ void shouldAddCardWhenDeckSumEqualsMinimum() { } @Test - @DisplayName("딜러는 카드의 합이 16을 초과하면 카드를 더 이상 뽑을 수 없다.") + @DisplayName("딜러가 카드의 합이 16을 초과한 상태로 카드를 뽑으려고 하면 예외가 발생한다.") void shouldThrowExceptionWhenDeckSumOverMinimum() { // given Dealer testDealer = createDealerWithCards( diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java index 8c2c1f95563..d2f119348b4 100644 --- a/src/test/java/domain/GameResultTest.java +++ b/src/test/java/domain/GameResultTest.java @@ -46,7 +46,7 @@ void shouldReturnWinTieLossResult() { Player player1 = playerIterator.next(); addCardsToParticipantDeck(player1, new Card(CardShape.DIAMOND, CardContents.J), - new Card(CardShape.DIAMOND, CardContents.THREE) + new Card(CardShape.DIAMOND, CardContents.A) ); Player player2 = playerIterator.next(); addCardsToParticipantDeck(player2, @@ -65,12 +65,12 @@ void shouldReturnWinTieLossResult() { ); Map expectPlayerWinLossResults = new LinkedHashMap<>(); - expectPlayerWinLossResults.put(player1, -1000L); + expectPlayerWinLossResults.put(player1, 1500L); expectPlayerWinLossResults.put(player2, -1000L); expectPlayerWinLossResults.put(player3, 1000L); expectPlayerWinLossResults.put(player4, 1000L); - long expectDealerWinLossResults = 0L; + long expectDealerWinLossResults = -2500L; // when GameResult gameResult = GameResult.calculate(dealer, players); From 23413c398a711daea58d787df16ec9bf69a14ed1 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 04:59:33 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix(GameResult):=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=B0=98=ED=99=98=20=EC=8B=9C=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=88=9C=EC=84=9C=EA=B0=80=20?= =?UTF-8?q?=EC=84=9E=EC=9D=B4=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Map.copyOf() 사용 시 LinkedHashMap의 순서가 보장되지 않는 문제 해결 - 순서를 유지하면서 불변성을 보장하도록 Collections.unmodifiableMap()으로 변경 --- src/main/java/domain/GameResult.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java index df74b491b74..f0c5ea22778 100644 --- a/src/main/java/domain/GameResult.java +++ b/src/main/java/domain/GameResult.java @@ -1,5 +1,6 @@ package domain; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -45,7 +46,7 @@ private static long calculateDealerResult(Map playerResults) { } public Map getPlayerResults() { - return Map.copyOf(playerResults); + return Collections.unmodifiableMap(playerResults); } public long getDealerResult() { From e003abd37c5a65eeafe87c13e78ca95c5ce78f03 Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 05:01:37 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=2021=EC=A0=90=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=20=EB=8F=84=EB=8B=AC=20=EC=8B=9C=20=EA=B2=8C=EC=9E=84=EC=9D=B4?= =?UTF-8?q?=20=EB=B9=84=EC=A0=95=EC=83=81=20=EC=A2=85=EB=A3=8C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cards 내부에서 21점 이상일 때 카드를 추가하면 예외를 던져 게임 진행이 막히던 문제 해결 - 불필요해진 21점 초과 예외 검증 로직 제거 및 README 요구사항 동기화 --- README.md | 1 - .../java/controller/BlackJackController.java | 29 +++++++++++----- src/main/java/domain/Cards.java | 3 -- src/main/java/view/OutputView.java | 3 +- src/test/java/domain/CardsTest.java | 34 ------------------- 5 files changed, 21 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index dc20f594b1c..e933786c1c8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ - [x] 카드 추가 분배는 다음의 공통 제약 조건 내에서만 가능하다. - [x] 도메인 규칙 검증 - - [x] 이미 카드 합계가 21 이상이라 더 이상 받을 수 없는 경우 예외 처리 - [x] 이미 보유하고 있는 카드와 동일한 카드를 중복해서 추가하려는 경우 예외 처리 - [x] 플레이어는 카드 합계가 21 미만이라면 계속해서 카드를 추가로 받을 수 있다. - [x] 사용자 입력값 유효성 검사 diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index 535ddb3bbb1..52b4fb8d4a5 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -114,8 +114,19 @@ private void drawCardPerPlayerAndShowResult(Game game) { } private void drawCardUntilBustOrStand(Game game, Player player) { - while (player.isDrawable() && wantToHit(player)) { - drawCardAndPrintResult(game, player); + boolean isPlayerDrawingCard = player.isDrawable(); + while (isPlayerDrawingCard) { + boolean wantToHit = wantToHit(player); + drawCardIfDrawableAndWantToHit(game, player, wantToHit); + isPlayerDrawingCard = player.isDrawable() && wantToHit; + ParticipantDto updatedPlayerDto = ParticipantDto.from(player); + showPlayerCards(updatedPlayerDto); + } + } + + private void drawCardIfDrawableAndWantToHit(Game game, Player player, boolean wantToHit) { + if (wantToHit) { + game.drawCardUnderCondition(player); } } @@ -138,19 +149,19 @@ private boolean askDrawCard(ParticipantDto participantDto) { return inputView.readHitOrStand(); } - private void drawCardAndPrintResult(Game game, Player player) { - game.drawCardUnderCondition(player); - ParticipantDto updatedPlayerDto = ParticipantDto.from(player); - showPlayerCards(updatedPlayerDto); - } - private void showPlayerCards(ParticipantDto participantDto) { outputView.printCardShareDetail(participantDto); } private void checkAndAdjustDealerCards(Game game) { Dealer dealer = game.getDealer(); - boolean hasDealerDrawnMoreCard = game.drawCardUnderCondition(dealer); + while (dealer.isDrawable()) { + boolean hasDealerDrawnMoreCard = game.drawCardUnderCondition(dealer); + printDescriptionIfDealerDrewCard(hasDealerDrawnMoreCard); + } + } + + private void printDescriptionIfDealerDrewCard(boolean hasDealerDrawnMoreCard) { if (hasDealerDrawnMoreCard) { outputView.printAdditionalCardForDealerDescription(); } diff --git a/src/main/java/domain/Cards.java b/src/main/java/domain/Cards.java index ca708a46316..64760c7e5b0 100644 --- a/src/main/java/domain/Cards.java +++ b/src/main/java/domain/Cards.java @@ -20,9 +20,6 @@ public void addCard(Card card) { } private void validateAddable(Card card) { - if (calculateCardScoreSum() >= BUST_CRITERIA) { - throw new IllegalStateException("이미 카드 합계가 21 이상입니다. 카드를 추가할 수 없습니다."); - } if (cards.contains(card)) { throw new IllegalArgumentException("이미 보유하고 있는 카드입니다. 중복 추가할 수 없습니다."); } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index d35771d1c17..77ddd47933e 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -41,17 +41,16 @@ public void printInitialCardShareDetail(ParticipantDto dealerDto, List cards.addCard(newCard)) - .isInstanceOf(IllegalStateException.class); - } - - @Test - @DisplayName("카드 합계가 21점을 초과할 때 카드를 추가로 뽑으면 예외가 발생한다.") - void shouldThrowExceptionWhenDeckSumOverMaximum() { - // given - Cards cards = createCardsWithCards( - new Card(CardShape.SPADE, CardContents.TEN), - new Card(CardShape.CLOVER, CardContents.TEN), - new Card(CardShape.DIAMOND, CardContents.TWO) - ); - - Card newCard = new Card(CardShape.HEART, CardContents.THREE); - - // when & then - assertThatThrownBy(() -> cards.addCard(newCard)) - .isInstanceOf(IllegalStateException.class); - } - @Test @DisplayName("카드 목록에 이미 존재하는 카드를 중복으로 추가하면 예외가 발생한다.") void shouldThrowExceptionForDuplicatedCard() { From 5dddc2c52d916c007db3f8bc15d057743b01266c Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 05:46:15 +0900 Subject: [PATCH 19/20] =?UTF-8?q?refactor:=20=EC=9B=90=EC=8B=9C=EA=B0=92?= =?UTF-8?q?=20=ED=8F=AC=EC=9E=A5=20=EB=B0=8F=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게임의 최종 수익 정산 결과를 원시 타입 Long 대신 Profit 래퍼 객체로 포장 - BlackJackController가 필드로 가지던 CardShuffleStrategy를 메서드 파라미터로 분리해 컨트롤러 인스턴스 변수에서 제거 --- src/main/java/Main.java | 7 ++-- .../java/controller/BlackJackController.java | 10 ++--- src/main/java/domain/BetMoney.java | 4 -- src/main/java/domain/GameResult.java | 38 +++++++++---------- src/main/java/domain/Profit.java | 4 ++ src/main/java/dto/GameResultDto.java | 13 ++++--- src/test/java/domain/BetMoneyTest.java | 2 +- src/test/java/domain/GameResultTest.java | 14 +++---- 8 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 src/main/java/domain/Profit.java diff --git a/src/main/java/Main.java b/src/main/java/Main.java index dd48579d88a..570b363ff7a 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -8,10 +8,9 @@ public class Main { public static void main(String[] args) { InputView inputView = new InputView(); OutputView outputView = new OutputView(); - CardShuffleStrategy strategy = new RandomCardShuffleStrategy(); + BlackJackController controller = new BlackJackController(inputView, outputView); - BlackJackController controller = new BlackJackController(inputView, outputView, strategy); - - controller.doGame(); + CardShuffleStrategy strategy = new RandomCardShuffleStrategy(); + controller.doGame(strategy); } } diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index 52b4fb8d4a5..883342ba89f 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -18,16 +18,14 @@ public class BlackJackController { private final InputView inputView; private final OutputView outputView; - private final CardShuffleStrategy strategy; - public BlackJackController(InputView inputView, OutputView outputView, CardShuffleStrategy strategy) { + public BlackJackController(InputView inputView, OutputView outputView) { this.inputView = inputView; this.outputView = outputView; - this.strategy = strategy; } - public void doGame() { - Game game = setUpGame(); + public void doGame(CardShuffleStrategy strategy) { + Game game = setUpGame(strategy); drawInitialCardsAndShowResult(game); @@ -38,7 +36,7 @@ public void doGame() { showGameResult(game); } - private Game setUpGame() { + private Game setUpGame(CardShuffleStrategy strategy) { Players players = generatePlayers(); return Game.registerParticipantsAndPrepareTotalDeck(players, strategy); } diff --git a/src/main/java/domain/BetMoney.java b/src/main/java/domain/BetMoney.java index 935b5bf3e44..8a32fe1f7e6 100644 --- a/src/main/java/domain/BetMoney.java +++ b/src/main/java/domain/BetMoney.java @@ -10,8 +10,4 @@ private void validateIsPositive(int betAmount) { throw new IllegalArgumentException("배팅 금액은 양수여야 합니다."); } } - - public int getBetAmount() { - return betAmount; - } } diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java index f0c5ea22778..25d06df6aaa 100644 --- a/src/main/java/domain/GameResult.java +++ b/src/main/java/domain/GameResult.java @@ -5,19 +5,19 @@ import java.util.Map; public class GameResult { - private final Map playerResults; - private final long dealerResult; + private final Map playerProfits; + private final Profit dealerProfit; - private GameResult(Map playerResults, long dealerResult) { - this.playerResults = playerResults; - this.dealerResult = dealerResult; + private GameResult(Map playerProfits, Profit dealerProfit) { + this.playerProfits = playerProfits; + this.dealerProfit = dealerProfit; } public static GameResult calculate(Dealer dealer, Players players) { validateCalculatable(dealer); - Map playerResults = calculatePlayerResults(dealer, players); - long dealerResult = calculateDealerResult(playerResults); - return new GameResult(playerResults, dealerResult); + Map playerProfits = calculatePlayerResults(dealer, players); + Profit dealerProfit = calculateDealerResult(playerProfits); + return new GameResult(playerProfits, dealerProfit); } private static void validateCalculatable(Dealer dealer) { @@ -26,30 +26,30 @@ private static void validateCalculatable(Dealer dealer) { } } - private static Map calculatePlayerResults(Dealer dealer, Players players) { - Map playerWinTieLossResults = new LinkedHashMap<>(); + private static Map calculatePlayerResults(Dealer dealer, Players players) { + Map playerWinTieLossResults = new LinkedHashMap<>(); for (Player player : players) { PlayerResult playerResult = PlayerResult.determinePlayerResult(dealer, player); double rate = playerResult.getReturnRate(); long profit = player.calculateBettingProfit(rate); - playerWinTieLossResults.put(player, profit); + playerWinTieLossResults.put(player, new Profit(profit)); } return playerWinTieLossResults; } - private static long calculateDealerResult(Map playerResults) { + private static Profit calculateDealerResult(Map playerProfits) { long playerBettingProfits = 0; - for (long playerResult : playerResults.values()) { - playerBettingProfits += playerResult; + for (Profit playerProfit : playerProfits.values()) { + playerBettingProfits += playerProfit.profit(); } - return playerBettingProfits * (-1); + return new Profit(playerBettingProfits * (-1)); } - public Map getPlayerResults() { - return Collections.unmodifiableMap(playerResults); + public Map getPlayerProfits() { + return Collections.unmodifiableMap(playerProfits); } - public long getDealerResult() { - return dealerResult; + public long getDealerProfit() { + return dealerProfit.profit(); } } diff --git a/src/main/java/domain/Profit.java b/src/main/java/domain/Profit.java new file mode 100644 index 00000000000..a0927c70994 --- /dev/null +++ b/src/main/java/domain/Profit.java @@ -0,0 +1,4 @@ +package domain; + +public record Profit(long profit) { +} diff --git a/src/main/java/dto/GameResultDto.java b/src/main/java/dto/GameResultDto.java index f1e3c40dfe8..cda162c87b5 100644 --- a/src/main/java/dto/GameResultDto.java +++ b/src/main/java/dto/GameResultDto.java @@ -2,6 +2,7 @@ import domain.GameResult; import domain.Player; +import domain.Profit; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; @@ -10,15 +11,15 @@ public record GameResultDto(Map playerWinTieLossResults, long dealerWinTieLossResult) { public static GameResultDto from(GameResult gameResult) { - Map playerResults = convertPlayerResult(gameResult.getPlayerResults()); - long dealerResults = gameResult.getDealerResult(); - return new GameResultDto(playerResults, dealerResults); + Map playerProfits = convertPlayerResult(gameResult.getPlayerProfits()); + long dealerProfit = gameResult.getDealerProfit(); + return new GameResultDto(playerProfits, dealerProfit); } - private static Map convertPlayerResult(Map playerWinTieLossResults) { + private static Map convertPlayerResult(Map playerWinTieLossResults) { Map playerResults = new LinkedHashMap<>(); - for (Entry entry : playerWinTieLossResults.entrySet()) { - playerResults.put(entry.getKey().getName(), entry.getValue()); + for (Entry entry : playerWinTieLossResults.entrySet()) { + playerResults.put(entry.getKey().getName(), entry.getValue().profit()); } return playerResults; } diff --git a/src/test/java/domain/BetMoneyTest.java b/src/test/java/domain/BetMoneyTest.java index 1c49bd55ae5..d68a9f81392 100644 --- a/src/test/java/domain/BetMoneyTest.java +++ b/src/test/java/domain/BetMoneyTest.java @@ -19,7 +19,7 @@ void shouldReturnBetMoneyForValidBetAmount() { BetMoney betMoney = new BetMoney(betAmount); // then - assertThat(betMoney.getBetAmount()).isEqualTo(betAmount); + assertThat(betMoney.betAmount()).isEqualTo(betAmount); } @ParameterizedTest diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java index d2f119348b4..2d96782335a 100644 --- a/src/test/java/domain/GameResultTest.java +++ b/src/test/java/domain/GameResultTest.java @@ -64,18 +64,18 @@ void shouldReturnWinTieLossResult() { new Card(CardShape.HEART, CardContents.NINE) ); - Map expectPlayerWinLossResults = new LinkedHashMap<>(); - expectPlayerWinLossResults.put(player1, 1500L); - expectPlayerWinLossResults.put(player2, -1000L); - expectPlayerWinLossResults.put(player3, 1000L); - expectPlayerWinLossResults.put(player4, 1000L); + Map expectPlayerWinLossResults = new LinkedHashMap<>(); + expectPlayerWinLossResults.put(player1, new Profit(1500L)); + expectPlayerWinLossResults.put(player2, new Profit(-1000L)); + expectPlayerWinLossResults.put(player3, new Profit(1000L)); + expectPlayerWinLossResults.put(player4, new Profit(1000L)); long expectDealerWinLossResults = -2500L; // when GameResult gameResult = GameResult.calculate(dealer, players); - Map playerResults = gameResult.getPlayerResults(); - long dealerResults = gameResult.getDealerResult(); + Map playerResults = gameResult.getPlayerProfits(); + long dealerResults = gameResult.getDealerProfit(); // then assertThat(playerResults).isEqualTo(expectPlayerWinLossResults); From f218f7a00249f617253459fab7813f21462713dd Mon Sep 17 00:00:00 2001 From: Yeji Kim Date: Sun, 15 Mar 2026 05:58:14 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e933786c1c8..5f31a8de203 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ classDiagram +generateGameResult() GameResult } class GameResult { - -Map~Player, Result~ playerResults - -Map~Result, Integer~ dealerResult + -Map~Player, Profit~ playerProfits + -Profit dealerProfit +calculate(Dealer, Players)$ GameResult } class Participant { @@ -113,6 +113,12 @@ classDiagram -String name } class Player { + -BetMoney betMoney + +calculateBettingProfit(double) Long + } + class BetMoney { + <> + -int betAmount } class Dealer { } @@ -138,11 +144,15 @@ classDiagram -String number -int score } - class Result { + class PlayerResult { <> - WIN, TIE, LOSS - +determinePlayerResult(...)$ Result - +reverse() Result + BLACKJACK, WIN, TIE, LOSS + -double returnRate + +determinePlayerResult(Dealer, Player)$ PlayerResult + } + class Profit { + <> + -long profit } class CardShuffleStrategy { <> @@ -157,8 +167,7 @@ classDiagram class BlackJackController { -InputView inputView -OutputView outputView - -CardShuffleStrategy strategy - +doGame() + +doGame(CardShuffleStrategy strategy) } class InputView { -Scanner scanner @@ -169,8 +178,8 @@ classDiagram %% 3. DTO 클래스 선언 class GameResultDto { <> - -Map~String, Integer~ dealerWinTieLossResult - -Map~String, String~ playerWinTieLossResults + -Map~String, Long~ playerWinTieLossResults + -long dealerWinTieLossResult } class ParticipantDto { <> @@ -189,25 +198,25 @@ classDiagram Game --> Deck Game --> Dealer Game --> Players - Game ..> GameResult : generates + Game ..> GameResult: generates Participant <|-- Player Participant <|-- Dealer Participant --> Name Participant --> Cards + Player --> BetMoney Players --> Player Cards --> Card Deck --> Card Card --> CardShape Card --> CardContents - GameResult --> Result + GameResult --> Profit + GameResult ..> PlayerResult: uses CardShuffleStrategy <|.. RandomCardShuffleStrategy - %% 컨트롤러 및 뷰 관계 Main --> BlackJackController BlackJackController --> InputView BlackJackController --> OutputView - BlackJackController --> CardShuffleStrategy - + BlackJackController ..> CardShuffleStrategy: uses %% DTO 관계 - GameResultDto --> ParticipantDto ParticipantDto --> CardDto +```