diff --git a/README.md b/README.md index 556099c4de3..09f7c164631 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,49 @@ -# java-blackjack +# ♠️ java-blackjack ♠️ -블랙잭 미션 저장소 +- 블랙잭 게임을 변형한 프로그램을 구현한다. +- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. +- 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. +- 게임을 완료한 후 각 플레이어별로 승패를 출력한다. -## 우아한테크코스 코드리뷰 +# 🛠️ 기능 구현 목록 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +- [x] 입력 + - [x] 게임에 참여할 사람의 이름을 입력 받을 수 있다. + - [x] 한 장 더 받을지 여부를 입력 받을 수 있다. +- [x] 입력 검증 + - [x] 카드 추가 여부를 올바른 형태 (y/n)으로 입력했는지 검증할 수 있다. +- [x] 도메인 + - [x] 이름은 빈 문자열일 수 없다. + - [x] 게임 참가자의 핸드에 새로운 카드를 추가할 수 있다. + - [x] 이름이 중복되는 플레이어는 존재할 수 없다. + - [x] 플레이어가 없는 경우는 게임을 시작할 수 없다. + - [x] 게임 참가자는 딜러 제외 3명 이하여야 한다. + - [x] 카드합을 계산할 때 Ace 카드는 1 또는 11로 계산한다 + - [x] 카드합을 계산할 떄 J,Q,K카드는 각각 10으로 계산한다. + - [x] 점수를 계산할 때 Ace 카드가 있는 경우 21을 넘지 않으면서 가장 가깝도록 유리하게 계산한다 + - [x] 카드합 비교를 통해서 플레이어의 승패를 결정할 수 있다. + - [x] 카드합 비교를 통해서 딜러의 승패를 계산할 수 있다. + - [x] 딜러는 17점 미만이면 카드를 받아야 한다. + - [x] 딜러는 17점 이상이면 카드를 그만 받아야 한다. + - [x] 핸드에서 에이스가 몇개있는지 파악할 수 있다 + - [x] 핸드의 합을 계산할 수 있다. + - [x] 저지는 핸드에서 21에 가장 가까운 합을 구할 수 있다. + - [x] 핸드의 최소합이 21을 초과하면 플레이어는 버스트한다. + - [x] 핸드의 최소합이 21 이하면 플레이어는 카드를 뽑을지 여부를 선택할 수 있다. + - [x] 핸드에 카드를 추가할 수 있다. + - [x] 카드덱에서 카드를 지정한 개수만큼 건네줄 수 있다. + - [x] 카드덱에서 카드를 한 장 뽑아서 건네줄 수 있다. + - [x] 카드덱에서 보유한 카드 개수보다 많이 뽑으면 예외가 발생한다. + - [x] 참여자의 핸드에 초기 카드를 분배할 수 있다. + - [x] 플레이어는 10억까지 베팅할 수 있다. + - [x] 플레이어는 핸드를 완성시키는 단계에서 버스트 하면 금액을 모두 잃는다. + - [x] 처음 두 장의 카드 합이 21이 되는 블랙잭일 경우 베팅 금액의 1.5배를 추가로 얻는다. (소숫점 제거) + - [x] 플레이어가 블랙잭이고 딜러도 블랙잭이면 플레이어는 베팅 금액을 그대로 돌려받는다. + - [x] 플레이어가 딜러에게 승리하면 베팅 금액만큼을 추가로 얻는다. +- [x] 출력 + - [x] 각 참여자의 카드 정보를 출력할 수 있다. + - [x] 각 참여자의 카드 합을 출력할 수 있다. + - [x] 최종 승패를 출력할 수 있다. diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..47214ff1a6f --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,16 @@ +package blackjack; + +import blackjack.view.InputView; +import blackjack.view.MessageResolver; +import blackjack.view.OutputView; + +public class Application { + + public static void main(String[] args) { + InputView inputView = new InputView(new InputMapper()); + OutputView outputView = new OutputView(new MessageResolver()); + + BlackJackGame blackJackGame = new BlackJackGame(inputView, outputView); + blackJackGame.run(); + } +} diff --git a/src/main/java/blackjack/BlackJackGame.java b/src/main/java/blackjack/BlackJackGame.java new file mode 100644 index 00000000000..c3f3788f57a --- /dev/null +++ b/src/main/java/blackjack/BlackJackGame.java @@ -0,0 +1,57 @@ +package blackjack; + +import blackjack.domain.bet.PlayerBets; +import blackjack.domain.card.CardDeck; +import blackjack.domain.card.Hand; +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.player.PlayerName; +import blackjack.domain.player.Players; +import blackjack.domain.result.PlayerProfits; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.List; + +public class BlackJackGame { + + private final InputView inputView; + private final OutputView outputView; + + public BlackJackGame(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + CardDeck cardDeck = CardDeck.createShuffledDeck(); + Players players = initPlayers(cardDeck); + PlayerBets playerBets = inputView.readBetInformation(players); + Dealer dealer = new Dealer(Hand.createHandFrom(cardDeck)); + outputView.printParticipantInitialHand(dealer, players); + + completePlayersHand(players, cardDeck); + dealer.completeHand(cardDeck); + outputView.printCompletedHandsStatus(dealer, players); + + PlayerProfits playerProfits = playerBets.calculateProfitResult(dealer); + outputView.printProfitResults(playerProfits); + } + + private Players initPlayers(CardDeck cardDeck) { + List playerNames = inputView.readNames(); + return new Players(playerNames.stream() + .map(playerName -> new Player(playerName, Hand.createHandFrom(cardDeck))) + .toList()); + } + + private void completePlayersHand(Players players, CardDeck cardDeck) { + players.getPlayers().forEach(player -> completePlayerHand(player, cardDeck)); + } + + private void completePlayerHand(Player participant, CardDeck cardDeck) { + while (participant.canHit() && inputView.readDrawDecision(participant.getName()).isYes()) { + participant.appendCard(cardDeck.popCard()); + outputView.printPlayerHand(participant); + } + } +} diff --git a/src/main/java/blackjack/InputMapper.java b/src/main/java/blackjack/InputMapper.java new file mode 100644 index 00000000000..f108f887aae --- /dev/null +++ b/src/main/java/blackjack/InputMapper.java @@ -0,0 +1,22 @@ +package blackjack; + +import blackjack.domain.DrawDecision; +import blackjack.domain.player.PlayerName; +import java.util.Arrays; +import java.util.List; + +public class InputMapper { + + private static final String DELIMITER = ","; + + public List mapToPlayerNames(String target) { + String[] split = target.split(DELIMITER); + return Arrays.stream(split) + .map(PlayerName::new) + .toList(); + } + + public DrawDecision mapToDrawDecision(String target) { + return DrawDecision.from(target); + } +} diff --git a/src/main/java/blackjack/domain/DrawDecision.java b/src/main/java/blackjack/domain/DrawDecision.java new file mode 100644 index 00000000000..ed5b60b65df --- /dev/null +++ b/src/main/java/blackjack/domain/DrawDecision.java @@ -0,0 +1,26 @@ +package blackjack.domain; + +import java.util.Arrays; + +public enum DrawDecision { + + YES("y"), + NO("n"); + + private final String code; + + DrawDecision(String code) { + this.code = code; + } + + public static DrawDecision from(String code) { + return Arrays.stream(values()) + .filter(drawDecision -> drawDecision.code.equals(code)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] " + YES.code + "또는 " + NO.code + "로 입력해주세요")); + } + + public boolean isYes() { + return this == YES; + } +} diff --git a/src/main/java/blackjack/domain/Score.java b/src/main/java/blackjack/domain/Score.java new file mode 100644 index 00000000000..d3d63e4e495 --- /dev/null +++ b/src/main/java/blackjack/domain/Score.java @@ -0,0 +1,47 @@ +package blackjack.domain; + +import java.util.Objects; + +public class Score { + + private static final int MAX_SCORE = 21; + + private final int value; + + public Score(int value) { + this.value = value; + } + + public boolean isAbove(Score target) { + return this.value > target.value; + } + + public boolean isMaxScore() { + return value == MAX_SCORE; + } + + public boolean isBustScore() { + return value > MAX_SCORE; + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Score score = (Score) o; + return value == score.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/blackjack/domain/bet/BetAmout.java b/src/main/java/blackjack/domain/bet/BetAmout.java new file mode 100644 index 00000000000..a66d4d871ca --- /dev/null +++ b/src/main/java/blackjack/domain/bet/BetAmout.java @@ -0,0 +1,24 @@ +package blackjack.domain.bet; + +public class BetAmout { + + private static final int MAX = 1_000_000_000; + private static final int MIN = 1_000; + + private final int amount; + + public BetAmout(int amount) { + validateRange(amount); + this.amount = amount; + } + + public Profit calculateProfit(double leverage) { + return new Profit((int) (amount * leverage)); + } + + private void validateRange(int amount) { + if (amount < MIN || MAX < amount) { + throw new IllegalArgumentException("[ERROR] 베팅 금액은 " + MIN + "부터 " + MAX + "이하까지 가능합니다."); + } + } +} diff --git a/src/main/java/blackjack/domain/bet/PlayerBets.java b/src/main/java/blackjack/domain/bet/PlayerBets.java new file mode 100644 index 00000000000..f57787b2dc8 --- /dev/null +++ b/src/main/java/blackjack/domain/bet/PlayerBets.java @@ -0,0 +1,30 @@ +package blackjack.domain.bet; + +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.result.GameResult; +import blackjack.domain.result.PlayerProfits; +import java.util.Map; +import java.util.stream.Collectors; + +public class PlayerBets { + + private final Map playerBets; + + public PlayerBets(Map playerBets) { + this.playerBets = playerBets; + } + + public PlayerProfits calculateProfitResult(Dealer dealer) { + return new PlayerProfits(playerBets.keySet().stream() + .collect(Collectors.toMap( + player -> player, + player -> (calculatePlayerProfit(player, dealer))))); + } + + private Profit calculatePlayerProfit(Player player, Dealer dealer) { + GameResult gameResult = GameResult.of(dealer, player); + BetAmout betAmout = playerBets.get(player); + return betAmout.calculateProfit(gameResult.getProfitLeverage()); + } +} diff --git a/src/main/java/blackjack/domain/bet/Profit.java b/src/main/java/blackjack/domain/bet/Profit.java new file mode 100644 index 00000000000..9632aacee6a --- /dev/null +++ b/src/main/java/blackjack/domain/bet/Profit.java @@ -0,0 +1,41 @@ +package blackjack.domain.bet; + +import java.util.Objects; + +public class Profit { + + private final int value; + + public Profit(int value) { + this.value = value; + } + + public Profit add(Profit other) { + return new Profit(this.value + other.value); + } + + public Profit inverse() { + return new Profit(-1 * value); + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Profit profit = (Profit) o; + return value == profit.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/blackjack/domain/card/Card.java b/src/main/java/blackjack/domain/card/Card.java new file mode 100644 index 00000000000..9d6b960bcbf --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,43 @@ +package blackjack.domain.card; + +import java.util.Objects; + +public class Card { + + private final CardShape cardShape; + private final CardNumber cardNumber; + + public Card(CardShape cardShape, CardNumber cardNumber) { + this.cardShape = cardShape; + this.cardNumber = cardNumber; + } + + public boolean isAce() { + return cardNumber.isAce(); + } + + public CardNumber getCardNumber() { + return cardNumber; + } + + public CardShape getCardShape() { + return cardShape; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Card card = (Card) o; + return cardShape == card.cardShape && cardNumber == card.cardNumber; + } + + @Override + public int hashCode() { + return Objects.hash(cardShape, cardNumber); + } +} diff --git a/src/main/java/blackjack/domain/card/CardDeck.java b/src/main/java/blackjack/domain/card/CardDeck.java new file mode 100644 index 00000000000..8ad67fa1b08 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardDeck.java @@ -0,0 +1,38 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class CardDeck { + + private final List cards; + + CardDeck(List cards) { + this.cards = cards; + } + + public static CardDeck createShuffledDeck() { + List cards = Arrays.stream(CardShape.values()) + .flatMap(shape -> Arrays.stream(CardNumber.values()) + .map(number -> new Card(shape, number))) + .collect(Collectors.toList()); + Collections.shuffle(cards); + return new CardDeck(cards); + } + + public Card popCard() { + if (cards.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 남아있는 카드가 부족하여 카드를 뽑을 수 없습니다"); + } + return cards.remove(cards.size() - 1); + } + + public List popCards(int count) { + return IntStream.range(0, count) + .mapToObj(i -> popCard()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/blackjack/domain/card/CardNumber.java b/src/main/java/blackjack/domain/card/CardNumber.java new file mode 100644 index 00000000000..e93fae8cc77 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardNumber.java @@ -0,0 +1,33 @@ +package blackjack.domain.card; + +public enum CardNumber { + + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10), + QUEEN(10), + KING(10); + + private final int value; + + CardNumber(int value) { + this.value = value; + } + + public boolean isAce() { + return this == ACE; + } + + public int getValue() { + return value; + } +} + diff --git a/src/main/java/blackjack/domain/card/CardShape.java b/src/main/java/blackjack/domain/card/CardShape.java new file mode 100644 index 00000000000..24735fa598a --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardShape.java @@ -0,0 +1,9 @@ +package blackjack.domain.card; + +public enum CardShape { + + HEART, + DIAMOND, + CLUB, + SPADE; +} diff --git a/src/main/java/blackjack/domain/card/Hand.java b/src/main/java/blackjack/domain/card/Hand.java new file mode 100644 index 00000000000..77a499f5bd0 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Hand.java @@ -0,0 +1,57 @@ +package blackjack.domain.card; + +import blackjack.domain.Score; +import java.util.List; + +public class Hand { + + public static final int INITIAL_HAND_SIZE = 2; + private static final int BUST_THRESHOLD = 21; + private static final int ACE_WEIGHT = 10; + + private final List cards; + + Hand(List cards) { + this.cards = cards; + } + + public static Hand createHandFrom(CardDeck cardDeck) { + return new Hand(cardDeck.popCards(INITIAL_HAND_SIZE)); + } + + public Score calculateScore() { + int sum = calculateCardSummation(); + if (hasAce() && (sum + ACE_WEIGHT) <= BUST_THRESHOLD) { + sum += ACE_WEIGHT; + } + return new Score(sum); + } + + public int calculateCardSummation() { + return cards.stream() + .map(Card::getCardNumber) + .mapToInt(CardNumber::getValue) + .sum(); + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + public void appendCard(Card card) { + cards.add(card); + } + + public boolean isBlackJack() { + return calculateScore().isMaxScore() && cards.size() == INITIAL_HAND_SIZE; + } + + public int countDraw() { + return cards.size() - INITIAL_HAND_SIZE; + } + + public List getCards() { + return List.copyOf(cards); + } +} diff --git a/src/main/java/blackjack/domain/player/Dealer.java b/src/main/java/blackjack/domain/player/Dealer.java new file mode 100644 index 00000000000..b8292a8dc4a --- /dev/null +++ b/src/main/java/blackjack/domain/player/Dealer.java @@ -0,0 +1,25 @@ +package blackjack.domain.player; + +import blackjack.domain.card.CardDeck; +import blackjack.domain.card.Hand; + +public class Dealer extends Participant { + + private static final String DEALER_NAME = "딜러"; + public static final int HIT_THRESHOLD = 16; + + public Dealer(Hand hand) { + super(new PlayerName(DEALER_NAME), hand); + } + + @Override + public boolean canHit() { + return hand.calculateCardSummation() <= HIT_THRESHOLD; + } + + public void completeHand(CardDeck cardDeck) { + while (canHit()) { + appendCard(cardDeck.popCard()); + } + } +} diff --git a/src/main/java/blackjack/domain/player/Participant.java b/src/main/java/blackjack/domain/player/Participant.java new file mode 100644 index 00000000000..9ffb9d06f04 --- /dev/null +++ b/src/main/java/blackjack/domain/player/Participant.java @@ -0,0 +1,80 @@ +package blackjack.domain.player; + +import blackjack.domain.Score; +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import java.util.Objects; + +public abstract class Participant { + + protected final PlayerName name; + protected final Hand hand; + + protected Participant(PlayerName name, Hand hand) { + this.name = name; + this.hand = hand; + } + + public abstract boolean canHit(); + + public void appendCard(Card card) { + hand.appendCard(card); + } + + public Score calculateHandScore() { + return hand.calculateScore(); + } + + public int countDraw() { + return hand.countDraw(); + } + + public boolean isBusted() { + return hand.calculateScore().isBustScore(); + } + + public boolean isNotBusted() { + return !hand.calculateScore().isBustScore(); + } + + public boolean hasBlackJackHand() { + return hand.isBlackJack(); + } + + public boolean hasNoBlackJackHand() { + return !hand.isBlackJack(); + } + + public boolean hasScoreAbove(Participant other) { + return this.calculateHandScore().isAbove(other.calculateHandScore()); + } + + public boolean hasSameScore(Participant other) { + return this.calculateHandScore().equals(other.calculateHandScore()); + } + + public String getName() { + return name.getValue(); + } + + public Hand getHand() { + return hand; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Participant participant = (Participant) o; + return Objects.equals(name, participant.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/blackjack/domain/player/Player.java b/src/main/java/blackjack/domain/player/Player.java new file mode 100644 index 00000000000..9711b317b28 --- /dev/null +++ b/src/main/java/blackjack/domain/player/Player.java @@ -0,0 +1,17 @@ +package blackjack.domain.player; + +import blackjack.domain.card.Hand; + +public class Player extends Participant { + + public static final int HIT_THRESHOLD = 21; + + public Player(PlayerName name, Hand hand) { + super(name, hand); + } + + @Override + public boolean canHit() { + return hand.calculateCardSummation() <= HIT_THRESHOLD; + } +} diff --git a/src/main/java/blackjack/domain/player/PlayerName.java b/src/main/java/blackjack/domain/player/PlayerName.java new file mode 100644 index 00000000000..fa868c3da22 --- /dev/null +++ b/src/main/java/blackjack/domain/player/PlayerName.java @@ -0,0 +1,40 @@ +package blackjack.domain.player; + +import java.util.Objects; + +public class PlayerName { + + private final String value; + + public PlayerName(String value) { + validateNotEmpty(value); + this.value = value; + } + + private void validateNotEmpty(String value) { + if (value.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 이름이 빈 문자열입니다."); + } + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlayerName that = (PlayerName) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/blackjack/domain/player/Players.java b/src/main/java/blackjack/domain/player/Players.java new file mode 100644 index 00000000000..b76573739c3 --- /dev/null +++ b/src/main/java/blackjack/domain/player/Players.java @@ -0,0 +1,49 @@ +package blackjack.domain.player; + +import java.util.List; + +public class Players { + + private static final int MAX_COUNT = 3; + + private final List players; + + public Players(List players) { + validate(players); + this.players = players; + } + + private void validate(List participants) { + validateEachPlayerNameUnique(participants); + validateEntryNotEmpty(participants); + validatePlayerCountRange(participants); + } + + private void validateEachPlayerNameUnique(List participants) { + if (countUniquePlayer(participants) != participants.size()) { + throw new IllegalArgumentException("[ERROR] 중복되는 플레이어의 이름이 존재합니다"); + } + } + + private int countUniquePlayer(List participants) { + return (int) participants.stream() + .distinct() + .count(); + } + + private void validateEntryNotEmpty(List participants) { + if (participants.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 플레이어가 없습니다"); + } + } + + private void validatePlayerCountRange(List participants) { + if (participants.size() > MAX_COUNT) { + throw new IllegalArgumentException("[ERROR] 플레이어의 수는 " + MAX_COUNT + "이하여야 합니다"); + } + } + + public List getPlayers() { + return players; + } +} diff --git a/src/main/java/blackjack/domain/result/GameResult.java b/src/main/java/blackjack/domain/result/GameResult.java new file mode 100644 index 00000000000..a854c886c9f --- /dev/null +++ b/src/main/java/blackjack/domain/result/GameResult.java @@ -0,0 +1,34 @@ +package blackjack.domain.result; + +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import java.util.Arrays; +import java.util.function.BiPredicate; + +public enum GameResult { + + BLACKJACK_WIN(1.5, (dealer, player) -> dealer.hasNoBlackJackHand() && player.hasBlackJackHand()), + PLAYER_LOSE(-1.0, (dealer, player) -> player.isBusted() || dealer.hasScoreAbove(player)), + PLAYER_WIN(1.0, (dealer, player) -> (player.isNotBusted() && (dealer.isBusted() || player.hasScoreAbove(dealer)))), + PUSH(0.0, (dealer, player) -> player.isNotBusted() && dealer.isNotBusted() && player.hasSameScore(dealer)); + + + private final double profitLeverage; + private final BiPredicate biPredicate; + + GameResult(double profitLeverage, BiPredicate biPredicate) { + this.profitLeverage = profitLeverage; + this.biPredicate = biPredicate; + } + + public static GameResult of(Dealer dealer, Player player) { + return Arrays.stream(values()) + .filter(gameResult -> gameResult.biPredicate.test(dealer, player)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[INTERNAL ERROR] 게임 결과를 판정할 수 없습니다")); + } + + public double getProfitLeverage() { + return profitLeverage; + } +} diff --git a/src/main/java/blackjack/domain/result/PlayerProfits.java b/src/main/java/blackjack/domain/result/PlayerProfits.java new file mode 100644 index 00000000000..7120ac26ec9 --- /dev/null +++ b/src/main/java/blackjack/domain/result/PlayerProfits.java @@ -0,0 +1,31 @@ +package blackjack.domain.result; + +import blackjack.domain.bet.Profit; +import blackjack.domain.player.Player; +import java.util.Map; + +public class PlayerProfits { + + private final Map playerProfits; + + public PlayerProfits(Map playerProfits) { + this.playerProfits = playerProfits; + } + + public Profit findProfitOfPlayer(Player player) { + return playerProfits.get(player); + } + + public Profit calculateTotalProfit() { + return playerProfits.values().stream() + .reduce(new Profit(0), Profit::add); + } + + public Profit calculateDealerProfit() { + return calculateTotalProfit().inverse(); + } + + public Map getPlayerProfits() { + return playerProfits; + } +} diff --git a/src/main/java/blackjack/view/CardDescription.java b/src/main/java/blackjack/view/CardDescription.java new file mode 100644 index 00000000000..0527ff8ee64 --- /dev/null +++ b/src/main/java/blackjack/view/CardDescription.java @@ -0,0 +1,45 @@ +package blackjack.view; + +import static blackjack.domain.card.CardNumber.ACE; +import static blackjack.domain.card.CardNumber.EIGHT; +import static blackjack.domain.card.CardNumber.FIVE; +import static blackjack.domain.card.CardNumber.FOUR; +import static blackjack.domain.card.CardNumber.JACK; +import static blackjack.domain.card.CardNumber.KING; +import static blackjack.domain.card.CardNumber.NINE; +import static blackjack.domain.card.CardNumber.QUEEN; +import static blackjack.domain.card.CardNumber.SEVEN; +import static blackjack.domain.card.CardNumber.SIX; +import static blackjack.domain.card.CardNumber.TEN; +import static blackjack.domain.card.CardNumber.THREE; +import static blackjack.domain.card.CardNumber.TWO; +import static blackjack.domain.card.CardShape.CLUB; +import static blackjack.domain.card.CardShape.DIAMOND; +import static blackjack.domain.card.CardShape.HEART; +import static blackjack.domain.card.CardShape.SPADE; + +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; +import java.util.Map; + +public class CardDescription { + + private CardDescription() { + } + + public static final Map SHAPE_NAME = Map.of( + HEART, "하트", + SPADE, "스페이드", + DIAMOND, "다이아몬드", + CLUB, "클로버" + ); + public static final Map NUMBER_NAME = Map.ofEntries( + Map.entry(ACE, "A"), Map.entry(TWO, "2"), + Map.entry(THREE, "3"), Map.entry(FOUR, "4"), + Map.entry(FIVE, "5"), Map.entry(SIX, "6"), + Map.entry(SEVEN, "7"), Map.entry(EIGHT, "8"), + Map.entry(NINE, "9"), Map.entry(TEN, "10"), + Map.entry(JACK, "J"), Map.entry(QUEEN, "Q"), + Map.entry(KING, "K") + ); +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..9e080f342f6 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,45 @@ +package blackjack.view; + +import blackjack.InputMapper; +import blackjack.domain.DrawDecision; +import blackjack.domain.bet.BetAmout; +import blackjack.domain.bet.PlayerBets; +import blackjack.domain.player.Player; +import blackjack.domain.player.PlayerName; +import blackjack.domain.player.Players; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class InputView { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + + private final InputMapper inputMapper; + private final Scanner scanner = new Scanner(System.in); + + public InputView(InputMapper inputMapper) { + this.inputMapper = inputMapper; + } + + public List readNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + return inputMapper.mapToPlayerNames(scanner.nextLine()); + } + + public DrawDecision readDrawDecision(String name) { + String message = String.format("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)", name); + System.out.println(String.join("", LINE_SEPARATOR, message)); + return inputMapper.mapToDrawDecision(scanner.nextLine()); + } + + public PlayerBets readBetInformation(Players players) { + return new PlayerBets(players.getPlayers().stream() + .collect(Collectors.toMap(player -> player, this::readBetAmount))); + } + + public BetAmout readBetAmount(Player player) { + System.out.println(String.format("%s의 배팅 금액은?", player.getName())); + return new BetAmout(Integer.parseInt(scanner.nextLine())); + } +} diff --git a/src/main/java/blackjack/view/MessageResolver.java b/src/main/java/blackjack/view/MessageResolver.java new file mode 100644 index 00000000000..896964d0e94 --- /dev/null +++ b/src/main/java/blackjack/view/MessageResolver.java @@ -0,0 +1,129 @@ +package blackjack.view; + +import static blackjack.domain.card.Hand.INITIAL_HAND_SIZE; +import static blackjack.view.CardDescription.NUMBER_NAME; +import static blackjack.view.CardDescription.SHAPE_NAME; + +import blackjack.domain.bet.Profit; +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; +import blackjack.domain.card.Hand; +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Participant; +import blackjack.domain.player.Player; +import blackjack.domain.player.Players; +import blackjack.domain.result.PlayerProfits; +import java.util.Map; +import java.util.stream.Collectors; + +public class MessageResolver { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String SEPARATOR = ", "; + + public String resolvePlayerHandMessage(Participant participant) { + return String.format("%s 카드: %s", participant.getName(), resolveHandMessage(participant.getHand())); + } + + public String resolveInitialHandsMessage(Dealer dealer, Players players) { + return new StringBuilder() + .append(resolveHandOutEventMessage(players)) + .append(LINE_SEPARATOR) + .append(resolveDealerHandMessage(dealer)) + .append(LINE_SEPARATOR) + .append(resolvePlayersHandMessage(players)) + .toString(); + } + + public String resolveCompletedHandsMessage(Dealer dealer, Players players) { + return new StringBuilder() + .append(resolveDealerPopCountMessage(dealer)) + .append(resolvePlayerScore(dealer)) + .append(LINE_SEPARATOR) + .append(resolvePlayersScore(players)) + .toString(); + } + + public String resolveProfitResultMessage(PlayerProfits playerProfits) { + return new StringBuilder() + .append(LINE_SEPARATOR) + .append("##최종 수익") + .append(LINE_SEPARATOR) + .append(resolveParticipantProfitMessage(playerProfits.calculateDealerProfit())) + .append(LINE_SEPARATOR) + .append(resolvePlayerProfitMessage(playerProfits)) + .toString(); + } + + private String resolveHandOutEventMessage(Players players) { + String namesMessage = resolveNamesMessage(players); + String message = String.format("딜러와 %s에게 %d장을 나누었습니다.", namesMessage, INITIAL_HAND_SIZE); + return String.join("", LINE_SEPARATOR, message); + } + + private String resolveNamesMessage(Players players) { + return players.getPlayers().stream() + .map(Player::getName) + .collect(Collectors.joining(SEPARATOR)); + } + + private String resolveDealerHandMessage(Dealer dealer) { + Card card = dealer.getHand().getCards().get(0); + return String.format("%s: %s", dealer.getName(), resolveCardMessage(card)); + } + + private String resolvePlayersHandMessage(Players players) { + return players.getPlayers().stream() + .map(this::resolvePlayerHandMessage) + .collect(Collectors.joining(LINE_SEPARATOR)); + } + + private String resolveHandMessage(Hand hand) { + return hand.getCards().stream() + .map(this::resolveCardMessage) + .collect(Collectors.joining(SEPARATOR)); + } + + private String resolveCardMessage(Card card) { + CardNumber cardNumber = card.getCardNumber(); + CardShape cardShape = card.getCardShape(); + return String.format("%s%s", NUMBER_NAME.get(cardNumber), SHAPE_NAME.get(cardShape)); + } + + private String resolveDealerPopCountMessage(Dealer dealer) { + if (dealer.countDraw() > 0) { + String message = String.format("딜러는 %d이하라 %d장의 카드를 더 받았습니다.", Dealer.HIT_THRESHOLD, dealer.countDraw()); + return String.join("", LINE_SEPARATOR, message, LINE_SEPARATOR); + } + return ""; + } + + private String resolvePlayersScore(Players players) { + return players.getPlayers().stream() + .map(this::resolvePlayerScore) + .collect(Collectors.joining(LINE_SEPARATOR)); + } + + private String resolvePlayerScore(Participant participant) { + String handMessage = resolvePlayerHandMessage(participant); + return String.format("%s - 결과: %d", handMessage, participant.calculateHandScore().getValue()); + } + + private String resolveParticipantProfitMessage(Profit dealerProfit) { + return String.format("딜러: %d", dealerProfit.getValue()); + } + + private String resolvePlayerProfitMessage(PlayerProfits playerProfits) { + Map playerProfitMap = playerProfits.getPlayerProfits(); + return playerProfitMap.entrySet().stream() + .map(this::resolveSingleProfitMessage) + .collect(Collectors.joining(LINE_SEPARATOR)); + } + + private String resolveSingleProfitMessage(Map.Entry playerProfit) { + String playerName = playerProfit.getKey().getName(); + int profit = playerProfit.getValue().getValue(); + return String.format("%s: %d", playerName, profit); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000000..56635caa9e9 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,31 @@ +package blackjack.view; + +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.player.Players; +import blackjack.domain.result.PlayerProfits; + +public class OutputView { + + private final MessageResolver messageResolver; + + public OutputView(MessageResolver messageResolver) { + this.messageResolver = messageResolver; + } + + public void printParticipantInitialHand(Dealer dealer, Players players) { + System.out.println(messageResolver.resolveInitialHandsMessage(dealer, players)); + } + + public void printPlayerHand(Player player) { + System.out.println(messageResolver.resolvePlayerHandMessage(player)); + } + + public void printCompletedHandsStatus(Dealer dealer, Players players) { + System.out.println(messageResolver.resolveCompletedHandsMessage(dealer, players)); + } + + public void printProfitResults(PlayerProfits playerProfits) { + System.out.println(messageResolver.resolveProfitResultMessage(playerProfits)); + } +} diff --git a/src/test/java/blackjack/domain/DrawDecisionTest.java b/src/test/java/blackjack/domain/DrawDecisionTest.java new file mode 100644 index 00000000000..2eebb406034 --- /dev/null +++ b/src/test/java/blackjack/domain/DrawDecisionTest.java @@ -0,0 +1,43 @@ +package blackjack.domain; + +import static blackjack.domain.DrawDecision.NO; +import static blackjack.domain.DrawDecision.YES; +import static blackjack.domain.DrawDecision.from; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("드로우 결정 도메인 테스트") +class DrawDecisionTest { + + @DisplayName("존재하지 않는 코드명이면 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"1", "libi", "jerry"}) + void testEnumFromInvalidCode(String code) { + assertThatThrownBy(() -> from(code)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] y또는 n로 입력해주세요"); + } + + @DisplayName("존재하는 코드명이면 적절한 상수를 반환받는다") + @ParameterizedTest + @CsvSource(value = {"y, YES", "n, NO",}) + void testEnumFromValidCode(String code, DrawDecision drawDecision) { + assertThat(from(code)).isEqualTo(drawDecision); + } + + @DisplayName("YES인지 확인할 수 있다") + @Test + void testIsYes() { + assertAll( + () -> assertThat(YES.isYes()).isTrue(), + () -> assertThat(NO.isYes()).isFalse() + ); + } +} diff --git a/src/test/java/blackjack/domain/ScoreTest.java b/src/test/java/blackjack/domain/ScoreTest.java new file mode 100644 index 00000000000..90a39b3650c --- /dev/null +++ b/src/test/java/blackjack/domain/ScoreTest.java @@ -0,0 +1,45 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +@DisplayName("점수 테스트") +class ScoreTest { + + @DisplayName("더 낮은 점수와 비교할 수 있다") + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + void testScoreCompareWithBigger(int value) { + Score score = new Score(0); + Score target = new Score(value); + assertThat(score.isAbove(target)).isFalse(); + } + + @DisplayName("점수가 파라미터를 넘지 않는지 확인할 수 있다") + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + void testScoreCompareWithLower(int value) { + Score score = new Score(6); + Score target = new Score(value); + assertThat(score.isAbove(target)).isTrue(); + } + + @DisplayName("최고 점수(21)인지 확인할 수 있다") + @Test + void testIsMaxScore() { + Score score = new Score(21); + assertThat(score.isMaxScore()).isTrue(); + } + + @DisplayName("버스트 된 점수인지 확인할 수 있다") + @ParameterizedTest + @ValueSource(ints = {22, 23, 24, 25}) + void testIsBust(int scoreValue) { + Score score = new Score(scoreValue); + assertThat(score.isBustScore()).isTrue(); + } +} diff --git a/src/test/java/blackjack/domain/bet/BetAmoutTest.java b/src/test/java/blackjack/domain/bet/BetAmoutTest.java new file mode 100644 index 00000000000..931e5b48a0d --- /dev/null +++ b/src/test/java/blackjack/domain/bet/BetAmoutTest.java @@ -0,0 +1,39 @@ +package blackjack.domain.bet; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +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; + +@DisplayName("돈 테스트") +class BetAmoutTest { + + @DisplayName("생성 시 범위를 지키지 못하면 생성 검증에 실패한다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, 998, 888, 1_000_000_001}) + void testCreateMoneyWithInvalidRange(int amount) { + assertThatThrownBy(() -> new BetAmout(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 베팅 금액은 1000부터 1000000000이하까지 가능합니다."); + } + + + @DisplayName("생성 검증을 통과하면 생성에 성공한다") + @ParameterizedTest + @ValueSource(ints = {1000, 2000, 30000}) + void testCreateMoneyWithValidData(int amount) { + assertThatCode(() -> new BetAmout(amount)) + .doesNotThrowAnyException(); + } + + @DisplayName("돈에 특정 이율을 적용한 Profit을 계산할 수 있다") + @Test + void testCalculateProfit() { + BetAmout betAmout = new BetAmout(1000); + assertThat(betAmout.calculateProfit(1.5).getValue()).isEqualTo(1500); + } +} diff --git a/src/test/java/blackjack/domain/bet/PlayerBetsTest.java b/src/test/java/blackjack/domain/bet/PlayerBetsTest.java new file mode 100644 index 00000000000..680a1f6572c --- /dev/null +++ b/src/test/java/blackjack/domain/bet/PlayerBetsTest.java @@ -0,0 +1,40 @@ +package blackjack.domain.bet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.domain.card.TestHandCreator; +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.player.TestPlayerCreator; +import blackjack.domain.result.GameResult; +import blackjack.domain.result.PlayerProfits; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("베팅 관리자 도메인 테스트") +class PlayerBetsTest { + + @DisplayName("플레이어들의 통합 수익을 계산할 수 있다") + @Test + void testCalculatePlayerProfit() { + Player player1 = TestPlayerCreator.of("리비", 1, 10); + Player player2 = TestPlayerCreator.of("썬", 3, 4); + Dealer dealer = new Dealer(TestHandCreator.of(3, 4, 5)); + + Map playerMoneyMap = new HashMap<>(); + playerMoneyMap.put(player1, new BetAmout(1000)); + playerMoneyMap.put(player2, new BetAmout(1000)); + PlayerBets playerBets = new PlayerBets(playerMoneyMap); + + PlayerProfits playerProfits = playerBets.calculateProfitResult(dealer); + + assertThat(GameResult.of(dealer, player2)).isEqualTo(GameResult.PLAYER_LOSE); + assertAll( + () -> assertThat(playerProfits.findProfitOfPlayer(player1).getValue()).isEqualTo(1500), + () -> assertThat(playerProfits.findProfitOfPlayer(player2).getValue()).isEqualTo(-1000) + ); + } +} diff --git a/src/test/java/blackjack/domain/bet/ProfitTest.java b/src/test/java/blackjack/domain/bet/ProfitTest.java new file mode 100644 index 00000000000..25803f36191 --- /dev/null +++ b/src/test/java/blackjack/domain/bet/ProfitTest.java @@ -0,0 +1,25 @@ +package blackjack.domain.bet; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("수익 금액 테스트") +class ProfitTest { + + @DisplayName("수익에 다른 수익을 더할 수 있다") + @Test + void testAdd() { + Profit profit1 = new Profit(1000); + Profit profit2 = new Profit(-1000); + assertThat(profit1.add(profit2).getValue()).isEqualTo(0); + } + + @DisplayName("수익을 반전시킨 결과를 볼 수 있다") + @Test + void testInverse() { + Profit profit = new Profit(1000); + assertThat(profit.inverse().getValue()).isEqualTo(-1000); + } +} diff --git a/src/test/java/blackjack/domain/card/CardDeckTest.java b/src/test/java/blackjack/domain/card/CardDeckTest.java new file mode 100644 index 00000000000..1c6c858e7f7 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardDeckTest.java @@ -0,0 +1,77 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("총 카드 덱 테스트") +class CardDeckTest { + + @DisplayName("덱에서 카드를 뽑을 수 있다") + @Test + void testPopCardFromDeck() { + List cards = new ArrayList<>(); + Card card1 = new Card(CardShape.HEART, CardNumber.TWO); + Card card2 = new Card(CardShape.CLUB, CardNumber.THREE); + Card card3 = new Card(CardShape.DIAMOND, CardNumber.FOUR); + + cards.add(card1); + cards.add(card2); + cards.add(card3); + + CardDeck cardDeck = new CardDeck(cards); + Card popped = cardDeck.popCard(); + assertThat(popped).isEqualTo(card3); + } + + @DisplayName("덱에서 횟수만큼 카드를 뽑을 수 있다") + @Test + void testPopCardsFromDeck() { + List cards = new ArrayList<>(); + Card card1 = new Card(CardShape.HEART, CardNumber.TWO); + Card card2 = new Card(CardShape.CLUB, CardNumber.THREE); + Card card3 = new Card(CardShape.DIAMOND, CardNumber.FOUR); + + cards.add(card1); + cards.add(card2); + cards.add(card3); + + CardDeck cardDeck = new CardDeck(cards); + List popped = cardDeck.popCards(3); + + assertThat(popped).hasSize(3); + } + + @DisplayName("덱에서 카드를 하나 뽑는 경우 카드가 부족하면 예외가 발생한다") + @Test + void testInvalidPopInsufficientDeckCount() { + List cards = new ArrayList<>(); + CardDeck cardDeck = new CardDeck(cards); + + assertThatThrownBy(() -> cardDeck.popCard()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 남아있는 카드가 부족하여 카드를 뽑을 수 없습니다"); + } + + @DisplayName("덱에서 카드를 여러개 뽑는 경우 카드가 부족하면 예외가 발생한다") + @Test + void testInvalidPopCardsInsufficientDeckCount() { + List cards = new ArrayList<>(); + Card card1 = new Card(CardShape.HEART, CardNumber.TWO); + Card card2 = new Card(CardShape.CLUB, CardNumber.THREE); + Card card3 = new Card(CardShape.DIAMOND, CardNumber.FOUR); + + cards.add(card1); + cards.add(card2); + cards.add(card3); + + CardDeck cardDeck = new CardDeck(cards); + assertThatThrownBy(() -> cardDeck.popCards(4)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 남아있는 카드가 부족하여 카드를 뽑을 수 없습니다"); + } +} diff --git a/src/test/java/blackjack/domain/card/CardNumberTest.java b/src/test/java/blackjack/domain/card/CardNumberTest.java new file mode 100644 index 00000000000..0a9493f1bdb --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardNumberTest.java @@ -0,0 +1,20 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("카드 숫자 enum 테스트") +class CardNumberTest { + + @DisplayName("Ace에 해당하는 지 확인할 수 있다") + @Test + void testIsCardNumberAce() { + assertAll( + () -> assertThat(CardNumber.ACE.isAce()).isTrue(), + () -> assertThat(CardNumber.TWO.isAce()).isFalse() + ); + } +} diff --git a/src/test/java/blackjack/domain/card/HandTest.java b/src/test/java/blackjack/domain/card/HandTest.java new file mode 100644 index 00000000000..a624aefd1e1 --- /dev/null +++ b/src/test/java/blackjack/domain/card/HandTest.java @@ -0,0 +1,84 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("참가자 소유 카드 테스트") +class HandTest { + + @DisplayName("초기 핸드의 사이즈는 2이다") + @Test + void testCreateInitialHandSize() { + Hand hand = Hand.createHandFrom(CardDeck.createShuffledDeck()); + assertThat(hand.getCards().size()).isEqualTo(2); + } + + @DisplayName("카드의 합을 계산할 수 있다.") + @Test + void testHandSummation() { + Hand hand = TestHandCreator.of(2, 2, 2); + assertThat(hand.calculateCardSummation()).isEqualTo(6); + } + + @DisplayName("특정 카드를 핸드에 추가할 수 있다") + @Test + void testAppendCard() { + Card card = TestCardCreator.from(2); + Hand hand = TestHandCreator.of(); + hand.appendCard(card); + assertThat(hand.getCards()).containsExactly(card); + } + + @DisplayName("몇개의 카드를 더 뽑은 상태인지 확인할 수 있다") + @Test + void testCountPop() { + CardDeck cardDeck = CardDeck.createShuffledDeck(); + Hand hand = Hand.createHandFrom(cardDeck); + hand.appendCard(cardDeck.popCard()); + assertThat(hand.countDraw()).isEqualTo(1); + } + + @DisplayName("적절한 점수를 계산할 수 있다 - Ace 카드 없음") + @Test + void testCalculateScoreWithNoAce() { + Hand hand = TestHandCreator.of(2, 3, 4, 5); + assertThat(hand.calculateScore().getValue()).isEqualTo(14); + } + + @DisplayName("적절한 점수를 계산할 수 있다 - Ace 카드가 11로 이용됨") + @Test + void testCalculateScoreWithBigAce() { + Hand hand = TestHandCreator.of(1, 10); + assertThat(hand.calculateScore().getValue()).isEqualTo(21); + } + + @DisplayName("적절한 점수를 계산할 수 있다 - Ace 카드가 1로 이용됨") + @Test + void testCalculateScoreWithLowAce() { + Hand hand = TestHandCreator.of(1, 10, 2); + assertThat(hand.calculateScore().getValue()).isEqualTo(13); + } + + @DisplayName("적절한 점수를 계산할 수 있다 - Ace 카드 두개 이상") + @Test + void testCalculateScoreWithMultipleAce() { + Hand hand = TestHandCreator.of(1, 1, 1); + assertThat(hand.calculateScore().getValue()).isEqualTo(13); + } + + @DisplayName("손패가 블랙잭인지 알 수 있다") + @Test + void testIsBlackJack() { + Hand hand = TestHandCreator.of(1, 10); + assertThat(hand.isBlackJack()).isTrue(); + } + + @DisplayName("손패의 점수가 21이더라도 드로우 한 이력이 있으면 블랙잭이 아니다") + @Test + void testIsNotBlackJack() { + Hand hand = TestHandCreator.of(2, 9, 10); + assertThat(hand.isBlackJack()).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/card/TestCardCreator.java b/src/test/java/blackjack/domain/card/TestCardCreator.java new file mode 100644 index 00000000000..677f45952fb --- /dev/null +++ b/src/test/java/blackjack/domain/card/TestCardCreator.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +import static blackjack.domain.card.CardShape.HEART; + +import java.util.Arrays; + +public class TestCardCreator { + + private TestCardCreator() { + } + + public static Card from(int number) { + CardNumber cardNumber = Arrays.stream(CardNumber.values()) + .filter(cn -> cn.getValue() == number) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + return new Card(HEART, cardNumber); + } +} diff --git a/src/test/java/blackjack/domain/card/TestCardDeckCreator.java b/src/test/java/blackjack/domain/card/TestCardDeckCreator.java new file mode 100644 index 00000000000..bf66e1fd35c --- /dev/null +++ b/src/test/java/blackjack/domain/card/TestCardDeckCreator.java @@ -0,0 +1,16 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class TestCardDeckCreator { + + private TestCardDeckCreator() { + } + + public static CardDeck createFrom(int... cards) { + return new CardDeck(Arrays.stream(cards) + .mapToObj(TestCardCreator::from) + .collect(Collectors.toList())); + } +} diff --git a/src/test/java/blackjack/domain/card/TestHandCreator.java b/src/test/java/blackjack/domain/card/TestHandCreator.java new file mode 100644 index 00000000000..ff070e6e694 --- /dev/null +++ b/src/test/java/blackjack/domain/card/TestHandCreator.java @@ -0,0 +1,16 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class TestHandCreator { + + private TestHandCreator() { + } + + public static Hand of(int... numbers) { + return new Hand(Arrays.stream(numbers) + .mapToObj(TestCardCreator::from) + .collect(Collectors.toList())); + } +} diff --git a/src/test/java/blackjack/domain/player/DealerTest.java b/src/test/java/blackjack/domain/player/DealerTest.java new file mode 100644 index 00000000000..bd0d894087e --- /dev/null +++ b/src/test/java/blackjack/domain/player/DealerTest.java @@ -0,0 +1,40 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.CardDeck; +import blackjack.domain.card.TestCardDeckCreator; +import blackjack.domain.card.TestHandCreator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayName("딜러 테스트") +class DealerTest { + + @DisplayName("딜러는 16점 이하이면 카드를 받을 수 있다") + @ParameterizedTest + @CsvSource(value = {"6, 10", "5, 10", "4, 10"}) + void testCannotHit(int card1, int card2) { + Dealer dealer = new Dealer(TestHandCreator.of(card1, card2)); + assertThat(dealer.canHit()).isTrue(); + } + + @DisplayName("딜러는 17점 이상이면 카드를 받을 수 없다") + @ParameterizedTest + @CsvSource(value = {"7, 10", "8, 10", "9, 10"}) + void testCanHit(int card1, int card2) { + Dealer dealer = new Dealer(TestHandCreator.of(card1, card2)); + assertThat(dealer.canHit()).isFalse(); + } + + @DisplayName("히트 규칙을 기반으로 덱을 완성할 수 있다") + @Test + void testCompleteDealerHand() { + Dealer dealer = new Dealer(TestHandCreator.of(4, 3)); + CardDeck deck = TestCardDeckCreator.createFrom(9, 10, 5, 4, 3); + dealer.completeHand(deck); + assertThat(dealer.calculateHandScore().getValue()).isEqualTo(19); + } +} diff --git a/src/test/java/blackjack/domain/player/ParticipantNameTest.java b/src/test/java/blackjack/domain/player/ParticipantNameTest.java new file mode 100644 index 00000000000..e2f523e93f6 --- /dev/null +++ b/src/test/java/blackjack/domain/player/ParticipantNameTest.java @@ -0,0 +1,18 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("참가자 이름 테스트") +class ParticipantNameTest { + + @DisplayName("이름은 빈 문자열일 수 없다") + @Test + void testCreatePlayerNameWithEmpty() { + assertThatThrownBy(() -> new PlayerName("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 이름이 빈 문자열입니다."); + } +} diff --git a/src/test/java/blackjack/domain/player/PlayerTest.java b/src/test/java/blackjack/domain/player/PlayerTest.java new file mode 100644 index 00000000000..0f4aba76fa9 --- /dev/null +++ b/src/test/java/blackjack/domain/player/PlayerTest.java @@ -0,0 +1,28 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.TestHandCreator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayName("블랙잭 플레이어 테스트") +class PlayerTest { + + @DisplayName("플레이어는 21이 넘으면 히트할 수 없다") + @ParameterizedTest + @CsvSource(value = {"2, 10, 10", "3, 10, 10", "4, 10, 10"}) + void testCannotHit(int card1, int card2, int card3) { + Player player = new Player(new PlayerName("썬"), TestHandCreator.of(card1, card2, card3)); + assertThat(player.canHit()).isFalse(); + } + + @DisplayName("플레이어는 21이 넘으면 히트할 수 없다") + @ParameterizedTest + @CsvSource(value = {"1, 10, 10", "2, 8, 10", "2, 7, 10"}) + void testCanHit(int card1, int card2, int card3) { + Player player = new Player(new PlayerName("썬"), TestHandCreator.of(card1, card2, card3)); + assertThat(player.canHit()).isTrue(); + } +} diff --git a/src/test/java/blackjack/domain/player/PlayersTest.java b/src/test/java/blackjack/domain/player/PlayersTest.java new file mode 100644 index 00000000000..c4a6b9f6f0b --- /dev/null +++ b/src/test/java/blackjack/domain/player/PlayersTest.java @@ -0,0 +1,54 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("참가자들 테스트") +class PlayersTest { + + @DisplayName("참가자들 중 이름이 중복되는 경우는 생성 검증에 실패한다") + @Test + void testCreatePlayersWithDuplicateNames() { + Player player1 = TestPlayerCreator.of("리비", 1, 2); + Player player2 = TestPlayerCreator.of("리비", 3, 4); + + assertThatThrownBy(() -> new Players(List.of(player1, player2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 중복되는 플레이어의 이름이 존재합니다"); + } + + @DisplayName("플레이어가 없으면 생성에 실패한다") + @Test + void testCreatePlayersWithEmptyEntry() { + assertThatThrownBy(() -> new Players(List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 플레이어가 없습니다"); + } + + @DisplayName("딜러를 제외한 게임 참여자가 3명을 넘는 경우는 생성 검증에 실패한다") + @Test + void testCreatePlayersWithInvalidEntryCount() { + Player player1 = TestPlayerCreator.of("리비", 1, 2); + Player player2 = TestPlayerCreator.of("제리", 3, 4); + Player player3 = TestPlayerCreator.of("잉크", 1, 2); + Player player4 = TestPlayerCreator.of("트레", 3, 4); + + assertThatThrownBy(() -> new Players(List.of(player1, player2, player3, player4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 플레이어의 수는 3이하여야 합니다"); + } + + @DisplayName("생성 검증을 모두 통과하면 생성에 성공한다") + @Test + void testCreateWithValidPlayers() { + Player player1 = TestPlayerCreator.of("리비", 1, 2); + Player player2 = TestPlayerCreator.of("제리", 3, 4); + + assertThatCode(() -> new Players(List.of(player1, player2))) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/blackjack/domain/player/TestPlayerCreator.java b/src/test/java/blackjack/domain/player/TestPlayerCreator.java new file mode 100644 index 00000000000..130762332e0 --- /dev/null +++ b/src/test/java/blackjack/domain/player/TestPlayerCreator.java @@ -0,0 +1,13 @@ +package blackjack.domain.player; + +import blackjack.domain.card.TestHandCreator; + +public class TestPlayerCreator { + + private TestPlayerCreator() { + } + + public static Player of(String name, int... hand) { + return new Player(new PlayerName(name), TestHandCreator.of(hand)); + } +} diff --git a/src/test/java/blackjack/domain/result/GameResultTest.java b/src/test/java/blackjack/domain/result/GameResultTest.java new file mode 100644 index 00000000000..58d15049a72 --- /dev/null +++ b/src/test/java/blackjack/domain/result/GameResultTest.java @@ -0,0 +1,58 @@ +package blackjack.domain.result; + +import static blackjack.domain.result.GameResult.BLACKJACK_WIN; +import static blackjack.domain.result.GameResult.PLAYER_LOSE; +import static blackjack.domain.result.GameResult.PLAYER_WIN; +import static blackjack.domain.result.GameResult.PUSH; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.TestHandCreator; +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.player.TestPlayerCreator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("게임 결과 테스트") +class GameResultTest { + + @DisplayName("플레이어 블랙잭이고 딜러가 블랙잭이 아니면 플레이어가 블랙잭 승리를 한다.") + @Test + void testPlayerBlackJackWin() { + Player player = TestPlayerCreator.of("리비", 1, 10); + Dealer dealer = new Dealer(TestHandCreator.of(3, 4)); + assertThat(GameResult.of(dealer, player)).isEqualTo(BLACKJACK_WIN); + } + + @DisplayName("딜러와 플레이어 모두 21점이더라도 블랙잭 핸드가 있다면 블랙잭쪽이 이긴다.") + @Test + void testPlayerBlackJackWinMaxScore() { + Player player = TestPlayerCreator.of("리비", 1, 10); + Dealer dealer = new Dealer(TestHandCreator.of(9, 2, 10)); + assertThat(GameResult.of(dealer, player)).isEqualTo(BLACKJACK_WIN); + } + + @DisplayName("딜러와 플레이어 모두 블랙잭인 경우 결과는 PUSH이다.") + @Test + void testPlayerAndDealerBlackJack() { + Player player = TestPlayerCreator.of("리비", 1, 10); + Dealer dealer = new Dealer(TestHandCreator.of(1, 10)); + assertThat(GameResult.of(dealer, player)).isEqualTo(PUSH); + } + + @DisplayName("딜러가 버스트 되고 플레이어가 살아있다면 플레이어가 승리한다") + @Test + void testPlayerWin() { + Player player = TestPlayerCreator.of("리비", 10, 10); + Dealer dealer = new Dealer(TestHandCreator.of(2, 10)); + assertThat(GameResult.of(dealer, player)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("딜러 플레이어 모두 버스트 되지 않은 경우 딜러의 점수보다 플레이어의 점수가 낮다면 플레이어가 패한다") + @Test + void testPlayerLose() { + Player player = TestPlayerCreator.of("리비", 2, 10); + Dealer dealer = new Dealer(TestHandCreator.of(10, 10)); + assertThat(GameResult.of(dealer, player)).isEqualTo(PLAYER_LOSE); + } +} diff --git a/src/test/java/blackjack/domain/result/PlayerProfitsTest.java b/src/test/java/blackjack/domain/result/PlayerProfitsTest.java new file mode 100644 index 00000000000..32a621ffbc3 --- /dev/null +++ b/src/test/java/blackjack/domain/result/PlayerProfitsTest.java @@ -0,0 +1,57 @@ +package blackjack.domain.result; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.bet.Profit; +import blackjack.domain.player.Player; +import blackjack.domain.player.TestPlayerCreator; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("플레이어의 수익 도메인 테스트") +class PlayerProfitsTest { + + @DisplayName("특정 플레이어의 수익을 조회할 수 있다") + @Test + void testFindProfitOfPlayer() { + Player player1 = TestPlayerCreator.of("썬", 1, 2, 3, 4); + Player player2 = TestPlayerCreator.of("리비", 3, 4); + + Map playerProfitMap = new HashMap<>(); + playerProfitMap.put(player1, new Profit(30)); + playerProfitMap.put(player2, new Profit(40)); + PlayerProfits playerProfits = new PlayerProfits(playerProfitMap); + + assertThat(playerProfits.findProfitOfPlayer(player1)).isEqualTo(new Profit(30)); + } + + @DisplayName("전체 플레이어들의 수익을 총합계산할 수 있다") + @Test + void testCalculateTotalProfit() { + Player player1 = TestPlayerCreator.of("썬", 1, 2, 3, 4); + Player player2 = TestPlayerCreator.of("리비", 3, 4); + + Map playerProfitMap = new HashMap<>(); + playerProfitMap.put(player1, new Profit(-30000)); + playerProfitMap.put(player2, new Profit(40000)); + PlayerProfits playerProfits = new PlayerProfits(playerProfitMap); + + assertThat(playerProfits.calculateTotalProfit().getValue()).isEqualTo(10000); + } + + @DisplayName("딜러의 수익을 계산할 수 있다") + @Test + void testCalculateDealerProfit() { + Player player1 = TestPlayerCreator.of("썬", 1, 2, 3, 4); + Player player2 = TestPlayerCreator.of("리비", 3, 4); + + Map playerProfitMap = new HashMap<>(); + playerProfitMap.put(player1, new Profit(-30000)); + playerProfitMap.put(player2, new Profit(40000)); + PlayerProfits playerProfits = new PlayerProfits(playerProfitMap); + + assertThat(playerProfits.calculateDealerProfit().getValue()).isEqualTo(-10000); + } +}