diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1c4e3cef883..71bbfc612fb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,24 +12,6 @@ - [ ] 미션의 필수 요구사항을 모두 구현했나요? - [ ] Gradle `test`를 실행했을 때, 모든 테스트가 정상적으로 통과했나요? - [ ] 애플리케이션이 정상적으로 실행되나요? -- [ ] [프롤로그](https://prolog.techcourse.co.kr)에 셀프 체크를 작성했나요? - - - - -## 객체지향 생활체조 요구사항을 얼마나 잘 충족했다고 생각하시나요? - -### 1~5점 중에서 선택해주세요. - -- [ ] 1 (전혀 충족하지 못함) -- [ ] 2 -- [ ] 3 (보통) -- [ ] 4 -- [ ] 5 (완벽하게 충족) - -### 선택한 점수의 이유를 적어주세요. - - - ## 어떤 부분에 집중하여 리뷰해야 할까요? diff --git a/README.md b/README.md index 0d964bda72d..f68a21e3969 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # 🃏 구현할 기능 목록 -### 1. 입력 처리: 플레이어 이름 +### 1. 입력 처리 - [x] 플레이어 이름을 쉼표를 구분자로 입력받는다. - [x] **검증**: 게임에 참여하는 플레이어 수가 5명 이하인지 검증한다. @@ -12,6 +12,9 @@ - [x] **검증**: 영문자만 포함되었는지 검증한다. - [x] **검증**: 플레이어 이름이 중복되지 않는지 검증한다. +- [x] 플레이어의 베팅 금액을 입력받는다. + - [x] **검증**: 숫자인지 검증한다. + ### 2. 카드 덱 생성 - [x] 52장의 카드 덱을 생성한다. @@ -58,13 +61,26 @@ - [x] 딜러와 플레이어 모두 21점 일 시, Blackjack 여부에 따라 승패가 처리된다. - [x] 딜러와 플레이어 모두 Blackjack이라면, 무승부 처리한다. -### 8. 결과 출력 +### 8. 베팅금 계산 + +- [x] 카드의 합이 21 초과인 경우 베팅 금액을 모두 잃는다. +- [x] 블랙잭인 경우 베팅 금액의 1.5배를 딜러에게 받는다. + - [x] 처음 두 장의 카드 합이 21인 경우만 블랙잭에 해당한다. + - [x] 딜러와 플레이어가 모두 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다. +- [x] 플레이어는 아래의 경우, 베팅 금액을 받는다. + - [x] 딜러가 21을 초과한 경우 + - [x] 플레이어가 승리한 경우 +### 9. 결과 출력 +**step1** - [x] 딜러와 플레이어의 모든 카드를 출력한다. - [x] 딜러와 플레이어의 최종 점수를 출력한다. - [x] 딜러 기준 승 / 무 / 패 결과를 집계하여 출력한다. - [x] 모든 플레이어 각각의 승패 결과를 출력한다. +**step2** +- [x] 딜러와 플레이어의 최종 수익을 출력한다. + # 📑 Domain 구조 및 역할 설계 **Card** - 필드 @@ -77,6 +93,11 @@ - `List hand` - 역할: 점수 합계 계산, 버스트 판단, Ace 1/11 처리 +**Money** +- 필드 + - `Integer money` +- 역할 : 베팅금 계산 + **Deck** - 필드 - `List cards` @@ -86,6 +107,7 @@ - `Dealer` 와 `Player`의 부모 클래스 - 필드 - `Hand hand` + - 'Money money' - 모든 참가자는 카드를 받음 **Dealer** @@ -121,4 +143,4 @@ - 일급 컬렉션을 쓴다. - 모든 엔티티를 작게 유지한다. - 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. -- 딜러와 플레이어에서 발생하는 중복 코드를 제거해야 한다. \ No newline at end of file +- 딜러와 플레이어에서 발생하는 중복 코드를 제거해야 한다. diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java index 9350ca9002f..ef5f6acabba 100644 --- a/src/main/java/controller/BlackJackController.java +++ b/src/main/java/controller/BlackJackController.java @@ -2,10 +2,10 @@ import domain.card.Deck; import domain.card.DefaultShuffleStrategy; -import domain.card.MatchResult; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; +import domain.money.BettingResult; import service.BlackJackService; import view.InputView; import view.OutputView; @@ -27,6 +27,7 @@ public void play() { Deck deck = new Deck(new DefaultShuffleStrategy()); Dealer dealer = new Dealer(); Players players = readUntilValidPlayers(); + readUntilValidMoney(players); BlackJackService blackJackService = new BlackJackService(deck, dealer, players); blackJackService.initHand(); @@ -35,9 +36,9 @@ public void play() { playRound(deck, dealer, players); outputView.showHandsResult(dealer, players); - Map playerResults = blackJackService.calculateResults(); - outputView.showDealerResult(blackJackService.calculateDealerResult(playerResults)); - outputView.showPlayerGameResult(playerResults); + Map bettingResults = blackJackService.calculateBettingResults(); + outputView.showDealerResult(blackJackService.calculateDealerResult(bettingResults)); + outputView.showPlayerGameResult(bettingResults); } private void playRound(Deck deck, Dealer dealer, Players players) { @@ -76,4 +77,11 @@ private Players readUntilValidPlayers() { } return new Players(players); } + + private void readUntilValidMoney(Players players) { + for (Player player : players.getPlayers()) { + int betAmount = inputView.readBettingAmount(player.getName()); + player.bet(betAmount); + } + } } diff --git a/src/main/java/domain/card/Deck.java b/src/main/java/domain/card/Deck.java index a4ecb18fb70..e4fa10f3676 100644 --- a/src/main/java/domain/card/Deck.java +++ b/src/main/java/domain/card/Deck.java @@ -8,7 +8,7 @@ public class Deck { public Deck(ShuffleStrategy shuffleStrategy) { for (Suit suit : Suit.values()) { - generateRank(suit); + generateCardBySuit(suit); } shuffleStrategy.shuffle(cards); @@ -22,7 +22,7 @@ public Card drawCard() { throw new IllegalArgumentException("더 이상 카드가 존재하지 않습니다."); } - private void generateRank(Suit suit) { + private void generateCardBySuit(Suit suit) { for (Rank rank : Rank.values()) { cards.add(new Card(rank, suit)); } diff --git a/src/main/java/domain/money/BettingResult.java b/src/main/java/domain/money/BettingResult.java new file mode 100644 index 00000000000..02aade0cdd1 --- /dev/null +++ b/src/main/java/domain/money/BettingResult.java @@ -0,0 +1,35 @@ +package domain.money; + +import domain.card.MatchResult; + +import static util.BlackJackConstant.BLACKJACK_MULTIPLIER; + +public class BettingResult { + + private final int earnings; + + private BettingResult(int earnings) { + this.earnings = earnings; + } + + public static BettingResult from(int betAmount, MatchResult matchResult, boolean isBlackJack) { + if (matchResult == MatchResult.WIN && isBlackJack) { + return new BettingResult((int) (betAmount * BLACKJACK_MULTIPLIER)); + } + if (matchResult == MatchResult.WIN) { + return new BettingResult(betAmount); + } + if (matchResult == MatchResult.LOSE) { + return new BettingResult(-betAmount); + } + return new BettingResult(0); + } + + public int getEarnings() { + return earnings; + } + + public int reverse() { + return -earnings; + } +} diff --git a/src/main/java/domain/money/Money.java b/src/main/java/domain/money/Money.java new file mode 100644 index 00000000000..be53022ebad --- /dev/null +++ b/src/main/java/domain/money/Money.java @@ -0,0 +1,21 @@ +package domain.money; + +public class Money { + + private final Integer money; + + public Money(Integer money) { + validate(money); + this.money = money; + } + + public Integer getValue() { + return money; + } + + private void validate(Integer money) { + if (money < 0) { + throw new IllegalArgumentException("돈은 음수가 될 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java index b7c9594d46f..17a5b15e02d 100644 --- a/src/main/java/domain/participant/Player.java +++ b/src/main/java/domain/participant/Player.java @@ -1,5 +1,8 @@ package domain.participant; +import domain.money.Money; + +import java.util.Optional; import java.util.regex.Pattern; import static util.BlackJackConstant.MAX_NAME_LENGTH; @@ -7,7 +10,9 @@ public class Player extends Participant { private static final String STRING_REGEX = "^[a-zA-Z]*$"; + private final String name; + private Optional money = Optional.empty(); public Player(String name) { validateNameLength(name); @@ -15,10 +20,22 @@ public Player(String name) { this.name = name; } + public void bet(Integer money) { + if (this.money.isPresent()) { + throw new IllegalArgumentException("베팅 금액은 변경할 수 없습니다."); + } + this.money = Optional.of(new Money(money)); + } + public String getName() { return name; } + public Money getMoney() { + return money.orElseThrow(() -> + new IllegalStateException("베팅 금액이 설정되지 않았습니다.")); + } + private void validateNameLength(String name) { if (name.isEmpty() || name.length() > MAX_NAME_LENGTH) { throw new IllegalArgumentException("플레이어 이름은 1글자 이상 8글자 이하여야 합니다."); diff --git a/src/main/java/service/BlackJackService.java b/src/main/java/service/BlackJackService.java index f4465e96acc..35599fa7040 100644 --- a/src/main/java/service/BlackJackService.java +++ b/src/main/java/service/BlackJackService.java @@ -5,8 +5,8 @@ import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; +import domain.money.BettingResult; -import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; @@ -45,11 +45,30 @@ public Map calculateResults() { return playerResults; } - public Map calculateDealerResult(Map playerResults) { - Map dealerResult = new EnumMap<>(MatchResult.class); + public Map calculateBettingResults() { + Map matchResults = calculateResults(); + Map bettingResults = new LinkedHashMap<>(); - for (MatchResult matchResult : playerResults.values()) { - dealerResult.put(matchResult.reverse(), dealerResult.getOrDefault(matchResult.reverse(), 0) + 1); + for (Player player : players.getPlayers()) { + MatchResult matchResult = matchResults.get(player.getName()); + + BettingResult result = BettingResult.from( + player.getMoney().getValue(), + matchResult, + player.getHand().isBlackJack() + ); + bettingResults.put(player.getName(), result); + } + + return bettingResults; + } + + public Integer calculateDealerResult(Map playerResults) { + int dealerResult = 0; + + for (BettingResult bettingResult : playerResults.values()) { + int reversedResult = bettingResult.reverse(); + dealerResult += reversedResult; } return dealerResult; @@ -109,6 +128,11 @@ private boolean handleBlackJack(Player player, Map playerRe playerResults.put(player.getName(), MatchResult.DRAW); return true; } + if (player.getHand().isBlackJack() && dealer.getHand().isBlackJack()) { + playerResults.put(player.getName(), MatchResult.DRAW); + return true; + } + return false; } } diff --git a/src/main/java/util/BlackJackConstant.java b/src/main/java/util/BlackJackConstant.java index b5e70a75cc8..9ced6e00c81 100644 --- a/src/main/java/util/BlackJackConstant.java +++ b/src/main/java/util/BlackJackConstant.java @@ -8,6 +8,7 @@ public class BlackJackConstant { public static final int DEALER_HIT_LIMIT = 16; public static final int MAX_NAME_LENGTH = 8; public static final int MAX_PLAYER_SIZE = 5; + public static final double BLACKJACK_MULTIPLIER = 1.5; private BlackJackConstant() { } diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index f73fcc2d900..840d764f17d 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -8,6 +8,7 @@ public class InputView { private static final String COMMA_DELIMITER = ","; private static final String BINARY_REGEX = "[yn]"; + private static final String BINARY_Y = "y"; private final Scanner sc = new Scanner(System.in); @@ -16,6 +17,18 @@ public List readPlayers() { return splitPlayerNames(userInput()); } + public int readBettingAmount(String name) { + while (true) { + try { + System.out.printf("%s의 배팅 금액은?%n", name); + String money = userInput(); + return Integer.parseInt(money); + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage("잘못된 입력입니다. 다시 입력해주세요."); + } + } + } + public boolean readPlayerToHitUntilValid(String name) { while (true) { try { @@ -42,7 +55,7 @@ private boolean validateBinaryOption(String userInput) { throw new IllegalArgumentException("잘못된 입력입니다. 다시 입력해주세요."); } - return userInput.equals("y"); + return userInput.equals(BINARY_Y); } private String userInput() { diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index 020d4773010..4933c71dec3 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -2,6 +2,7 @@ import domain.card.Card; import domain.card.MatchResult; +import domain.money.BettingResult; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; @@ -66,22 +67,19 @@ public void showHandsResult(Dealer dealer, Players players) { } } - public void showDealerResult(Map dealerResult) { + public void showDealerResult(int dealerResult) { StringBuilder dealerStatistics = new StringBuilder(); System.out.println("\n## 최종 승패"); - dealerStatistics.append("딜러: "); - for (Map.Entry results : dealerResult.entrySet()) { - printDealerStatistics(results, dealerStatistics); - } + dealerStatistics.append("딜러: ").append(dealerResult); System.out.println(dealerStatistics); } - public void showPlayerGameResult(Map playerResults) { - for (Map.Entry results : playerResults.entrySet()) { - System.out.printf("%s: %s\n", results.getKey(), results.getValue().getValue()); + public void showPlayerGameResult(Map playerResults) { + for (Map.Entry results : playerResults.entrySet()) { + System.out.printf("%s: %s\n", results.getKey(), results.getValue().getEarnings()); } } @@ -109,10 +107,4 @@ private StringBuilder printHand(Player player) { playerCards.append(String.join(", ", cards)); return playerCards; } - - private static void printDealerStatistics(Map.Entry results, StringBuilder result) { - if (results.getValue() != 0) { - result.append(String.format("%d%s ", results.getValue(), results.getKey().getValue())); - } - } } diff --git a/src/test/java/domain/DeckTest.java b/src/test/java/domain/DeckTest.java index 155f3a6fa87..2200eceded5 100644 --- a/src/test/java/domain/DeckTest.java +++ b/src/test/java/domain/DeckTest.java @@ -5,6 +5,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.HashSet; +import java.util.Set; + class DeckTest { @Test @@ -20,4 +23,23 @@ class DeckTest { Assertions.assertEquals(Rank.TWO, firstCard.getRank()); Assertions.assertEquals(Suit.HEART, firstCard.getSuit()); } + + @Test + @DisplayName("카드가 52장 모두 소진되어 더 이상 카드를 뽑을 수 없다면 에러가 나온다.") + void 카드모두_소진_시_에러() { + // given + Deck deck = new Deck(new NoShuffleStrategy()); + Set cards = new HashSet<>(); + + // when + for (int i = 0; i < 52; i++) { + Card card = deck.drawCard(); + cards.add(card); + } + + // then + Assertions.assertEquals(52, cards.size()); + + Assertions.assertThrows(IllegalArgumentException.class, deck::drawCard); + } } diff --git a/src/test/java/domain/MoneyTest.java b/src/test/java/domain/MoneyTest.java new file mode 100644 index 00000000000..2e87b30002b --- /dev/null +++ b/src/test/java/domain/MoneyTest.java @@ -0,0 +1,20 @@ +package domain; + +import domain.money.Money; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MoneyTest { + + @Test + @DisplayName("돈은 음수가 될 수 없다.") + void 돈이_음수인_경우_에러() { + // given + int negativeMoney = -3000; + + // when - then + Assertions.assertThrows(IllegalArgumentException.class, + () -> new Money(negativeMoney)); + } +} diff --git a/src/test/java/domain/PlayerTest.java b/src/test/java/domain/PlayerTest.java index b16b0ca3c50..27594f87b88 100644 --- a/src/test/java/domain/PlayerTest.java +++ b/src/test/java/domain/PlayerTest.java @@ -62,4 +62,28 @@ class PlayerTest { // then Assertions.assertEquals(1, player.getHand().getHand().size()); } + + @Test + @DisplayName("플레이어는 한 번만 베팅할 수 있어야 한다.") + void 플레이어_베팅_한_번_성공() { + // given + Player player = new Player("pobi"); + + // when + player.bet(30000); + } + + @Test + @DisplayName("플레이어는 베팅 금액은 두 번 이상 적용을 허용하지 않는다.") + void 플레이어_두번_베팅_실패() { + // given + Player player = new Player("pobi"); + + // when + player.bet(30000); + + // then + Assertions.assertThrows(IllegalArgumentException.class, + () -> player.bet(20000)); + } } diff --git a/src/test/java/service/BlackJackServiceTest.java b/src/test/java/service/BlackJackServiceTest.java index 633781f3f4e..aa7c3522445 100644 --- a/src/test/java/service/BlackJackServiceTest.java +++ b/src/test/java/service/BlackJackServiceTest.java @@ -5,11 +5,11 @@ import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; +import domain.money.BettingResult; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -227,28 +227,137 @@ class BlackJackServiceTest { } @Test - @DisplayName("딜러의 승패를 올바르게 판단한다.") - void 딜러_승패_판단() { + @DisplayName("딜러와 플레이어가 둘 다 블랙잭일 경우 무승부이다.") + void 동점_블랙잭_무승_판단() { // given Dealer dealer = new Dealer(); - Players players = new Players(List.of()); + Players players = new Players(List.of("pobi")); BlackJackService blackJackService = new BlackJackService( new Deck(new DefaultShuffleStrategy()), dealer, players); - Map playerResults = new HashMap<>(); + dealer.hit(new Card(Rank.ACE, Suit.CLOVER)); + dealer.hit(new Card(Rank.KING, Suit.CLOVER)); + + for (Player player : players.getPlayers()) { + player.hit(new Card(Rank.ACE, Suit.HEART)); + player.hit(new Card(Rank.JACK, Suit.CLOVER)); + } + + // when + Map matchResult = blackJackService.calculateResults(); + + // then + for (Map.Entry matchResultEntry : matchResult.entrySet()) { + Assertions.assertEquals(MatchResult.DRAW, matchResultEntry.getValue()); + } + } + + @Test + @DisplayName("플레이어가 일반 승리한 경우 베팅 금액을 받는다.") + void 플레이어_승리_베팅결과() { + // given + Dealer dealer = new Dealer(); + String name = "pobi"; + Players players = new Players(List.of(name)); + + BlackJackService blackJackService = new BlackJackService( + new Deck(new DefaultShuffleStrategy()), dealer, players); + + dealer.hit(new Card(Rank.JACK, Suit.CLOVER)); + dealer.hit(new Card(Rank.SEVEN, Suit.HEART)); + + Player player = players.getPlayers().getFirst(); + player.bet(10000); + + player.hit(new Card(Rank.NINE, Suit.HEART)); + player.hit(new Card(Rank.KING, Suit.CLOVER)); + + // when + Map results = blackJackService.calculateBettingResults(); + + // then + Assertions.assertEquals(10000, results.get(name).getEarnings()); + } + + @Test + @DisplayName("플레이어가 블랙잭으로 승리한 경우 베팅 금액의 1.5배를 받는다.") + void 플레이어_블랙잭_승리_베팅결과() { + // given + Dealer dealer = new Dealer(); + String name = "pobi"; + Players players = new Players(List.of(name)); + + BlackJackService blackJackService = new BlackJackService( + new Deck(new DefaultShuffleStrategy()), dealer, players); + + dealer.hit(new Card(Rank.JACK, Suit.CLOVER)); + dealer.hit(new Card(Rank.SEVEN, Suit.HEART)); + + Player player = players.getPlayers().getFirst(); + int money = 10000; + player.bet(money); + + player.hit(new Card(Rank.ACE, Suit.CLOVER)); + player.hit(new Card(Rank.KING, Suit.CLOVER)); + + // when + Map results = blackJackService.calculateBettingResults(); + + // then + Assertions.assertEquals(money * (1.5), results.get(name).getEarnings()); + } + + @Test + @DisplayName("플레이어가 패배한 경우 베팅 금액을 잃는다.") + void 플레이어_패배_베팅결과() { + // given + Dealer dealer = new Dealer(); + String name = "pobi"; + Players players = new Players(List.of(name)); + + BlackJackService blackJackService = new BlackJackService( + new Deck(new DefaultShuffleStrategy()), dealer, players); + + dealer.hit(new Card(Rank.ACE, Suit.CLOVER)); + dealer.hit(new Card(Rank.KING, Suit.CLOVER)); + + Player player = players.getPlayers().getFirst(); + int money = 10000; + player.bet(money); + player.hit(new Card(Rank.JACK, Suit.CLOVER)); + player.hit(new Card(Rank.SEVEN, Suit.HEART)); + + // when + Map results = blackJackService.calculateBettingResults(); + + // then + Assertions.assertEquals(money * (-1), results.get(name).getEarnings()); + } + + @Test + @DisplayName("플레이어가 무승부인 경우 베팅 금액을 돌려받는다.") + void 플레이어_무승부_베팅결과() { + // given + Dealer dealer = new Dealer(); + String name = "pobi"; + Players players = new Players(List.of(name)); + + BlackJackService blackJackService = new BlackJackService( + new Deck(new DefaultShuffleStrategy()), dealer, players); + + dealer.hit(new Card(Rank.JACK, Suit.CLOVER)); + dealer.hit(new Card(Rank.SEVEN, Suit.HEART)); - playerResults.put("pobi", MatchResult.WIN); - playerResults.put("sisi", MatchResult.WIN); - playerResults.put("ao", MatchResult.WIN); - playerResults.put("james", MatchResult.DRAW); - playerResults.put("lala", MatchResult.LOSE); + Player player = players.getPlayers().getFirst(); + int money = 10000; + player.bet(money); + player.hit(new Card(Rank.NINE, Suit.CLOVER)); + player.hit(new Card(Rank.EIGHT, Suit.CLOVER)); // when - Map matchResults = blackJackService.calculateDealerResult(playerResults); + Map results = blackJackService.calculateBettingResults(); // then - Assertions.assertEquals(3, matchResults.get(MatchResult.LOSE)); - Assertions.assertEquals(1, matchResults.get(MatchResult.DRAW)); - Assertions.assertEquals(1, matchResults.get(MatchResult.WIN)); + Assertions.assertEquals(0, results.get(name).getEarnings()); } }