diff --git a/README.md b/README.md index 556099c4de3..ba538864d25 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,54 @@ ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) + +## 기능 요구 사항 + +- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. +- 각 카드는 점수를 가진다. + - 숫자 카드는 카드 숫자로 계산한다. + - King, Queen, Jack은 각각 10으로 계산한다. + - Ace는 1 또는 11로 계산한다. +- 플레이어는 이름을 가진다. + - 이름은 공백으로만 구성될 수 없다. + - 이름은 앞뒤 공백을 가질 수 없다. + - 중복된 이름은 가질 수 없다. + +### 카드 승패에 따른 플레이어의 수익 + +- 플레이어가 배팅 금액의 1.5배를 얻는 경우 + - 플레이어가 '블랙잭'이고, 딜러가 블랙잭이 아닌 경우 +- 플레이어가 배팅 금액만큼 얻는 경우 + - 플레이어가 '블랙잭'이 아니면서, 딜러보다 점수가 높은 경우 +- 플레이어의 수익이 0인 경우 + - 플레이어, 딜러 모두 '블랙잭'인 경우 + - 플레이어, 딜러 모두 '블랙잭'이 아니면서, +- 플레이러가 배팅 금액만큼 잃는 경우 + - + +#### 딜러의 수익 + +- 플레이어가 수익을 얻는 만큼 잃는다. +- 플레이어가 수익을 잃은 만큼 얻는다. +- 모든 플레이어와 비교한 뒤에, 딜러가 얻은 수익들을 모두 더한다. + +#### 관련 용어 + +- 블랙잭 : 카드 2장 만으로 21점을 만든 경우 (ex. A스페이드, K스페이드) +- 버스트 : 카드 점수의 합이 21을 초과한 경우 (ex. K스페이드, K다이아몬드, 2스페이드) + +### 프로그램 진행 순서 + +1. 참가자 이름 입력한다. +2. 각 참가자의 배팅 금액을 입력받는다. +3. 카드를 2장씩 나눠준 후에, 현재 카드 상태 출력한다. (딜러는 1장만 출력) +4. 각 참가자들의 턴을 진행한다. + 1. 카드가 21을 넘은 경우, 턴이 종료된다. + 2. 카드를 더 받을 지 여부를 입력한다. + 3. 만약 더 받는다면, 카드를 한 장 추가하고 1으로 돌아간다. + 4. 더 받지 않을 경우, 턴을 종료한다. +5. 딜러의 턴을 진행한다. + 1. 카드가 16을 넘은 경우, 턴이 종료된다. + 2. 카드를 한 장 더 추가하고, 1로 돌아간다. +6. 딜러 및 모든 참가자의 보유 카드들과 점수를 출력한다. +7. 딜러 및 모든 참가자의 최종 수익을 출력한다. diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/java/blackjack/BlackJackApplication.java b/src/main/java/blackjack/BlackJackApplication.java new file mode 100644 index 00000000000..e2759f47eec --- /dev/null +++ b/src/main/java/blackjack/BlackJackApplication.java @@ -0,0 +1,12 @@ +package blackjack; + +public class BlackJackApplication { + + private BlackJackApplication() { + } + + public static void main(String[] args) { + BlackJackGame blackJackGame = new BlackJackGame(); + blackJackGame.run(); + } +} diff --git a/src/main/java/blackjack/BlackJackGame.java b/src/main/java/blackjack/BlackJackGame.java new file mode 100644 index 00000000000..3ee1204d424 --- /dev/null +++ b/src/main/java/blackjack/BlackJackGame.java @@ -0,0 +1,70 @@ +package blackjack; + +import blackjack.domain.card.Deck; +import blackjack.domain.money.Profit; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import blackjack.domain.participant.Players; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.List; +import java.util.Map; + +public class BlackJackGame { + + private final InputView inputView = new InputView(); + private final OutputView outputView = new OutputView(); + + public void run() { + Deck deck = Deck.createShuffledDeck(); + Dealer dealer = new Dealer(); + Players players = createPlayers(); + + drawStartCards(dealer, players, deck); + play(players, dealer, deck); + printResult(dealer, players); + } + + private Players createPlayers() { + List names = inputView.inputPlayerNames(); + List players = names.stream() + .map(name -> Player.from(name, inputView.inputBetAmount(name))) + .toList(); + return new Players(players); + } + + private void drawStartCards(Dealer dealer, Players players, Deck deck) { + dealer.drawStartCards(deck); + players.drawStartCards(deck); + outputView.printStartStatus(dealer, players); + } + + private void play(Players players, Dealer dealer, Deck deck) { + for (Player player : players.getPlayers()) { + playTurn(player, deck); + } + playTurn(dealer, deck); + } + + private void playTurn(Player player, Deck deck) { + while (player.isDrawable() && inputView.isPlayerWantDraw(player.getName())) { + player.add(deck.draw()); + outputView.printPlayerCards(player); + } + } + + private void playTurn(Dealer dealer, Deck deck) { + while (dealer.isDrawable()) { + outputView.printDealerDraw(); + dealer.add(deck.draw()); + } + } + + private void printResult(Dealer dealer, Players players) { + outputView.printEndingStatus(dealer, players); + + Profit dealerProfit = dealer.calculateDealerProfit(players); + Map playersProfit = dealer.calculatePlayersProfit(players); + outputView.printMatchResult(dealerProfit, playersProfit); + } +} 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..cb3f97f6509 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,60 @@ +package blackjack.domain.card; + +import java.util.Objects; +import java.util.StringJoiner; + +public class Card { + + private final Value value; + private final Shape shape; + + public Card(Value value, Shape shape) { + this.value = Objects.requireNonNull(value); + this.shape = Objects.requireNonNull(shape); + } + + public int getMinScore() { + return value.getMinScore(); + } + + public int getMaxScore() { + return value.getMaxScore(); + } + + public boolean isAce() { + return value.isAce(); + } + + public Value getValue() { + return value; + } + + public Shape getShape() { + return shape; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + Card card = (Card) object; + return value == card.value && shape == card.shape; + } + + @Override + public int hashCode() { + return Objects.hash(value, shape); + } + + @Override + public String toString() { + return new StringJoiner(", ", Card.class.getSimpleName() + "[", "]") + .add("value=" + value) + .add("shape=" + shape) + .toString(); + } +} diff --git a/src/main/java/blackjack/domain/card/Deck.java b/src/main/java/blackjack/domain/card/Deck.java new file mode 100644 index 00000000000..3c759f38d33 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,40 @@ +package blackjack.domain.card; + +import static java.util.stream.Collectors.toList; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class Deck { + + private final Queue cards; + + private Deck(List cards) { + this.cards = new LinkedList<>(cards); + } + + public static Deck createShuffledDeck() { + List cards = Arrays.stream(Shape.values()) + .map(Deck::makeCards) + .flatMap(List::stream) + .collect(toList()); + Collections.shuffle(cards); + return new Deck(cards); + } + + private static List makeCards(Shape shape) { + return Arrays.stream(Value.values()) + .map(value -> new Card(value, shape)) + .toList(); + } + + public Card draw() { + if (cards.isEmpty()) { + throw new IllegalStateException("카드를 더 이상 뽑을 수 없습니다."); + } + return cards.poll(); + } +} 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..707fbee837c --- /dev/null +++ b/src/main/java/blackjack/domain/card/Hand.java @@ -0,0 +1,69 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.List; + +public class Hand { + + public static final int BLACKJACK_SCORE = 21; + private static final int BLACKJACK_SIZE = 2; + + private final List cards; + + public Hand(List cards) { + this.cards = List.copyOf(cards); + } + + public Hand add(Card card) { + List newCards = new ArrayList<>(cards); + newCards.add(card); + + return new Hand(newCards); + } + + public int calculateScore() { + int totalMinScore = getMinScore(); + int biggerScore = getBiggerScore(); + + if (biggerScore > BLACKJACK_SCORE) { + return totalMinScore; + } + return biggerScore; + } + + private int getMinScore() { + return cards.stream() + .mapToInt(Card::getMinScore) + .sum(); + } + + private int getBiggerScore() { + int score = getMinScore(); + int differenceScore = cards.stream() + .filter(Card::isAce) + .mapToInt(this::calculateDifferenceScore) + .findAny() + .orElse(0); + return score + differenceScore; + } + + private int calculateDifferenceScore(Card card) { + return card.getMaxScore() - card.getMinScore(); + } + + public boolean isBusted() { + return calculateScore() > BLACKJACK_SCORE; + } + + public boolean isBlackjack() { + return cards.size() == BLACKJACK_SIZE && calculateScore() == BLACKJACK_SCORE; + } + + public List getCards() { + return cards; + } + + public boolean isEmpty() { + return cards.isEmpty(); + } +} diff --git a/src/main/java/blackjack/domain/card/Shape.java b/src/main/java/blackjack/domain/card/Shape.java new file mode 100644 index 00000000000..292d024bab4 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Shape.java @@ -0,0 +1,5 @@ +package blackjack.domain.card; + +public enum Shape { + SPADE, DIAMOND, HEART, CLOVER +} diff --git a/src/main/java/blackjack/domain/card/Value.java b/src/main/java/blackjack/domain/card/Value.java new file mode 100644 index 00000000000..7cd8f2898db --- /dev/null +++ b/src/main/java/blackjack/domain/card/Value.java @@ -0,0 +1,37 @@ +package blackjack.domain.card; + +public enum Value { + ACE(1, 11), + TWO(2, 2), + THREE(3, 3), + FOUR(4, 4), + FIVE(5, 5), + SIX(6, 6), + SEVEN(7, 7), + EIGHT(8, 8), + NINE(9, 9), + TEN(10, 10), + JACK(10, 10), + QUEEN(10, 10), + KING(10, 10); + + private final int minScore; + private final int maxScore; + + Value(int minScore, int maxScore) { + this.minScore = minScore; + this.maxScore = maxScore; + } + + public int getMinScore() { + return minScore; + } + + public int getMaxScore() { + return maxScore; + } + + public boolean isAce() { + return this == ACE; + } +} diff --git a/src/main/java/blackjack/domain/handrank/Blackjack.java b/src/main/java/blackjack/domain/handrank/Blackjack.java new file mode 100644 index 00000000000..3614a5082b6 --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/Blackjack.java @@ -0,0 +1,29 @@ +package blackjack.domain.handrank; + +import blackjack.domain.card.Hand; + +public final class Blackjack implements HankRank { + + @Override + public SingleMatchResult matchAtDealer(HankRank playerHandRank) { + if (playerHandRank.isBlackjack()) { + return SingleMatchResult.DRAW; + } + return SingleMatchResult.DEALER_WIN; + } + + @Override + public int getScore() { + return Hand.BLACKJACK_SCORE; + } + + @Override + public boolean isBlackjack() { + return true; + } + + @Override + public boolean isBust() { + return false; + } +} diff --git a/src/main/java/blackjack/domain/handrank/Bust.java b/src/main/java/blackjack/domain/handrank/Bust.java new file mode 100644 index 00000000000..f295fdccfca --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/Bust.java @@ -0,0 +1,45 @@ +package blackjack.domain.handrank; + +import blackjack.domain.card.Hand; + +public final class Bust implements HankRank { + + private final int score; + + public Bust(int score) { + validate(score); + this.score = score; + } + + private void validate(int score) { + if (score <= Hand.BLACKJACK_SCORE) { + throw new IllegalArgumentException("버스트 점수는 블랙잭 점수보다 높아야 합니다."); + } + } + + @Override + public SingleMatchResult matchAtDealer(HankRank playerHandRank) { + if (playerHandRank.isBust()) { + return SingleMatchResult.DEALER_WIN; + } + if (playerHandRank.isBlackjack()) { + return SingleMatchResult.PLAYER_BLACKJACK; + } + return SingleMatchResult.PLAYER_WIN; + } + + @Override + public int getScore() { + return score; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBust() { + return true; + } +} diff --git a/src/main/java/blackjack/domain/handrank/HandRankFactory.java b/src/main/java/blackjack/domain/handrank/HandRankFactory.java new file mode 100644 index 00000000000..270ecbc72c9 --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/HandRankFactory.java @@ -0,0 +1,21 @@ +package blackjack.domain.handrank; + +import blackjack.domain.card.Hand; + +public final class HandRankFactory { + + private static final HankRank BLACKJACK = new Blackjack(); + + private HandRankFactory() { + } + + public static HankRank from(Hand hand) { + if (hand.isBlackjack()) { + return BLACKJACK; + } + if (hand.isBusted()) { + return new Bust(hand.calculateScore()); + } + return new Stand(hand.calculateScore()); + } +} diff --git a/src/main/java/blackjack/domain/handrank/HankRank.java b/src/main/java/blackjack/domain/handrank/HankRank.java new file mode 100644 index 00000000000..803ae4f5f81 --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/HankRank.java @@ -0,0 +1,12 @@ +package blackjack.domain.handrank; + +public interface HankRank { + + SingleMatchResult matchAtDealer(HankRank playerHandRank); + + int getScore(); + + boolean isBlackjack(); + + boolean isBust(); +} diff --git a/src/main/java/blackjack/domain/handrank/SingleMatchResult.java b/src/main/java/blackjack/domain/handrank/SingleMatchResult.java new file mode 100644 index 00000000000..f75956567a3 --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/SingleMatchResult.java @@ -0,0 +1,22 @@ +package blackjack.domain.handrank; + +import blackjack.domain.money.BetAmount; +import blackjack.domain.money.Profit; + +public enum SingleMatchResult { + + PLAYER_BLACKJACK(1.5), + PLAYER_WIN(1), + DRAW(0), + DEALER_WIN(-1); + + private final double playerMultiplier; + + SingleMatchResult(double playerMultiplier) { + this.playerMultiplier = playerMultiplier; + } + + public Profit calculatePlayerProfit(BetAmount betAmount) { + return Profit.of(betAmount, playerMultiplier); + } +} diff --git a/src/main/java/blackjack/domain/handrank/Stand.java b/src/main/java/blackjack/domain/handrank/Stand.java new file mode 100644 index 00000000000..4e1641bcfc3 --- /dev/null +++ b/src/main/java/blackjack/domain/handrank/Stand.java @@ -0,0 +1,55 @@ +package blackjack.domain.handrank; + +import blackjack.domain.card.Hand; + +public final class Stand implements HankRank { + + private final int score; + + public Stand(int score) { + validate(score); + this.score = score; + } + + private void validate(int score) { + if (score > Hand.BLACKJACK_SCORE) { + throw new IllegalArgumentException("스탠드 점수는 블랙잭 점수보다 낮거나 같아야 합니다."); + } + } + + @Override + public SingleMatchResult matchAtDealer(HankRank playerHandRank) { + if (playerHandRank.isBlackjack()) { + return SingleMatchResult.PLAYER_BLACKJACK; + } + if (playerHandRank.isBust()) { + return SingleMatchResult.DEALER_WIN; + } + return matchThroughScore(playerHandRank); + } + + private SingleMatchResult matchThroughScore(HankRank playerHandRank) { + if (playerHandRank.getScore() > this.getScore()) { + return SingleMatchResult.PLAYER_WIN; + } + if (playerHandRank.getScore() == this.getScore()) { + return SingleMatchResult.DRAW; + } + return SingleMatchResult.DEALER_WIN; + } + + @Override + public int getScore() { + return score; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBust() { + return false; + } +} diff --git a/src/main/java/blackjack/domain/money/BetAmount.java b/src/main/java/blackjack/domain/money/BetAmount.java new file mode 100644 index 00000000000..a94118e4f55 --- /dev/null +++ b/src/main/java/blackjack/domain/money/BetAmount.java @@ -0,0 +1,21 @@ +package blackjack.domain.money; + +public class BetAmount { + + private final int value; + + public BetAmount(int value) { + validatePositive(value); + this.value = value; + } + + private void validatePositive(int value) { + if (value <= 0) { + throw new IllegalArgumentException("배팅 금액은 양수이어야 합니다."); + } + } + + public int toInt() { + return value; + } +} diff --git a/src/main/java/blackjack/domain/money/Profit.java b/src/main/java/blackjack/domain/money/Profit.java new file mode 100644 index 00000000000..6b1ca27ac47 --- /dev/null +++ b/src/main/java/blackjack/domain/money/Profit.java @@ -0,0 +1,47 @@ +package blackjack.domain.money; + +import java.util.Objects; + +public class Profit { + + public static final Profit ZERO = new Profit(0); + + private final int value; + + public Profit(int value) { + this.value = value; + } + + public static Profit of(BetAmount amount, double multiplier) { + return new Profit((int) (amount.toInt() * multiplier)); + } + + public Profit add(Profit profit) { + return new Profit(this.value + profit.value); + } + + public Profit reverse() { + return new Profit(-1 * value); + } + + public int toInt() { + return value; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + Profit profit = (Profit) object; + return value == profit.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/blackjack/domain/participant/Dealer.java b/src/main/java/blackjack/domain/participant/Dealer.java new file mode 100644 index 00000000000..06d75111e8c --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,49 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.money.Profit; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Dealer extends Participant { + + private static final int DRAWABLE_MAX_SCORE = 16; + private static final int START_CARD_SIZE = 1; + + public Dealer() { + super(Collections.emptyList()); + } + + Dealer(List cards) { + super(cards); + } + + public Profit calculateDealerProfit(Players players) { + Profit profit = Profit.ZERO; + for (Player player : players.getPlayers()) { + Profit dealerProfit = player.calculateProfit(this).reverse(); + profit = profit.add(dealerProfit); + } + return profit; + } + + public Map calculatePlayersProfit(Players players) { + Map totalResult = new LinkedHashMap<>(); + for (Player player : players.getPlayers()) { + totalResult.put(player, player.calculateProfit(this)); + } + return totalResult; + } + + @Override + protected int getMaxDrawableScore() { + return DRAWABLE_MAX_SCORE; + } + + @Override + protected int getStartCardSize() { + return START_CARD_SIZE; + } +} diff --git a/src/main/java/blackjack/domain/participant/Name.java b/src/main/java/blackjack/domain/participant/Name.java new file mode 100644 index 00000000000..3baa06e97a9 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Name.java @@ -0,0 +1,40 @@ +package blackjack.domain.participant; + +import java.util.Objects; + +public class Name { + + private final String name; + + public Name(String name) { + validate(name); + this.name = name.strip(); + } + + private void validate(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("이름은 적어도 한 글자 이상을 포함해야 합니다."); + } + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + Name name1 = (Name) object; + return Objects.equals(name, name1.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/blackjack/domain/participant/Participant.java b/src/main/java/blackjack/domain/participant/Participant.java new file mode 100644 index 00000000000..d66308212e9 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participant.java @@ -0,0 +1,60 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.Hand; +import blackjack.domain.handrank.HandRankFactory; +import blackjack.domain.handrank.HankRank; +import java.util.List; + +public abstract class Participant { + + protected static final int BLACKJACK_SCORE = 21; + private static final int START_CARDS_SIZE = 2; + + private Hand hand; + + protected Participant(List cards) { + this.hand = new Hand(cards); + } + + public final int calculateScore() { + return hand.calculateScore(); + } + + public final boolean isDrawable() { + return calculateScore() <= getMaxDrawableScore(); + } + + public final void drawStartCards(Deck deck) { + if (!hand.isEmpty()) { + throw new IllegalStateException("이미 시작 카드를 뽑았습니다."); + } + for (int i = 0; i < START_CARDS_SIZE; i++) { + add(deck.draw()); + } + } + + public final void add(Card card) { + if (!isDrawable()) { + throw new IllegalStateException("더 이상 카드를 추가할 수 없습니다."); + } + hand = hand.add(card); + } + + protected final HankRank getHandRank() { + return HandRankFactory.from(hand); + } + + public final List getStartCards() { + return getCards().subList(0, getStartCardSize()); + } + + public final List getCards() { + return hand.getCards(); + } + + protected abstract int getMaxDrawableScore(); + + protected abstract int getStartCardSize(); +} diff --git a/src/main/java/blackjack/domain/participant/Player.java b/src/main/java/blackjack/domain/participant/Player.java new file mode 100644 index 00000000000..e04aa84d775 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,54 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.handrank.SingleMatchResult; +import blackjack.domain.money.BetAmount; +import blackjack.domain.money.Profit; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Player extends Participant { + + private static final int START_CARD_SIZE = 2; + + private final Name name; + private final BetAmount betAmount; + + Player(List cards, Name name, BetAmount betAmount) { + super(cards); + this.name = Objects.requireNonNull(name); + this.betAmount = Objects.requireNonNull(betAmount); + } + + Player(List cards, Name name) { + this(cards, name, new BetAmount(1)); + } + + public static Player from(String name, int money) { + return new Player(Collections.emptyList(), new Name(name), new BetAmount(money)); + } + + public Profit calculateProfit(Dealer dealer) { + SingleMatchResult result = dealer.getHandRank().matchAtDealer(this.getHandRank()); + return result.calculatePlayerProfit(betAmount); + } + + public boolean isEqualName(Player player) { + return name.equals(player.name); + } + + @Override + protected int getMaxDrawableScore() { + return BLACKJACK_SCORE; + } + + @Override + protected int getStartCardSize() { + return START_CARD_SIZE; + } + + public String getName() { + return name.getName(); + } +} diff --git a/src/main/java/blackjack/domain/participant/Players.java b/src/main/java/blackjack/domain/participant/Players.java new file mode 100644 index 00000000000..710f47ead9d --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Players.java @@ -0,0 +1,56 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Deck; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +public class Players { + + private static final int MAX_PLAYERS_SIZE = 4; + + private final List players; + + public Players(List players) { + validateSize(players); + validateDistinct(players); + this.players = players; + } + + private void validateSize(List players) { + Objects.requireNonNull(players); + if (players.isEmpty()) { + throw new IllegalArgumentException("최소 한 명의 플레이어가 있어야 합니다."); + } + if (players.size() > MAX_PLAYERS_SIZE) { + throw new IllegalArgumentException("최대 4명의 플레이어만 참여 가능합니다."); + } + } + + private void validateDistinct(List players) { + if (isDuplicated(players)) { + throw new IllegalArgumentException("중복된 이름을 사용할 수 없습니다."); + } + } + + private boolean isDuplicated(List players) { + return IntStream.range(0, players.size()) + .anyMatch(index -> isDuplicated(players, index)); + } + + private boolean isDuplicated(List players, int index) { + return IntStream.range(0, players.size()) + .filter(currentIndex -> currentIndex != index) + .anyMatch(currentIndex -> players.get(currentIndex).isEqualName(players.get(index))); + } + + public void drawStartCards(Deck deck) { + for (Player player : players) { + player.drawStartCards(deck); + } + } + + public List getPlayers() { + return players; + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..b4efc96358b --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,48 @@ +package blackjack.view; + +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private static final Scanner SCANNER = new Scanner(System.in); + private static final String NAME_DELIMITER = ","; + private static final String WANT_DRAW_INPUT = "y"; + private static final String WANT_NOT_DRAW_INPUT = "n"; + + public List inputPlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String names = SCANNER.nextLine(); + return List.of(names.split(NAME_DELIMITER, -1)); + } + + public int inputBetAmount(String name) { + System.out.printf("%n%s의 배팅 금액은?%n", name); + return inputInt(); + } + + private int inputInt() { + try { + return Integer.parseInt(SCANNER.nextLine()); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException("숫자를 입력하여야 합니다.", exception); + } + } + + public boolean isPlayerWantDraw(String name) { + System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 %s, 아니오는 %s)%n", name, WANT_DRAW_INPUT, WANT_NOT_DRAW_INPUT); + String input = SCANNER.nextLine(); + return isWantDraw(input); + } + + private boolean isWantDraw(String input) { + if (WANT_DRAW_INPUT.equalsIgnoreCase(input)) { + return true; + } + if (WANT_NOT_DRAW_INPUT.equalsIgnoreCase(input)) { + return false; + } + throw new IllegalArgumentException("잘못된 입력입니다. 입력은 (%s/%s) 만 가능합니다." + .formatted(WANT_DRAW_INPUT, WANT_NOT_DRAW_INPUT)); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000000..f18fa21acdb --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,130 @@ +package blackjack.view; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import blackjack.domain.money.Profit; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import blackjack.domain.participant.Players; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class OutputView { + + private static final Map SHAPE_NAME = Map.of( + Shape.HEART, "하트", + Shape.SPADE, "스페이드", + Shape.DIAMOND, "다이아몬드", + Shape.CLOVER, "클로버" + ); + + private static final Map VALUE_NAME = Map.ofEntries( + Map.entry(Value.ACE, "A"), Map.entry(Value.TWO, "2"), + Map.entry(Value.THREE, "3"), Map.entry(Value.FOUR, "4"), + Map.entry(Value.FIVE, "5"), Map.entry(Value.SIX, "6"), + Map.entry(Value.SEVEN, "7"), Map.entry(Value.EIGHT, "8"), + Map.entry(Value.NINE, "9"), Map.entry(Value.TEN, "10"), + Map.entry(Value.JACK, "J"), Map.entry(Value.QUEEN, "Q"), + Map.entry(Value.KING, "K") + ); + + public void printStartStatus(Dealer dealer, Players players) { + System.out.println(); + System.out.println("딜러와 " + toPrintedFormat(players) + "에게 2장을 나누었습니다."); + printDealerCards(dealer.getStartCards()); + System.out.println(); + printPlayersCards(players); + System.out.println(); + } + + public void printEndingStatus(Dealer dealer, Players players) { + System.out.println(); + System.out.println(); + printDealerEndingStatus(dealer); + printPlayersEndingStatus(players); + System.out.println(); + } + + private void printDealerCards(List cards) { + System.out.print("딜러 카드: "); + printCards(cards); + } + + private void printPlayersCards(Players players) { + for (Player player : players.getPlayers()) { + printPlayerCards(player.getName(), player.getStartCards()); + System.out.println(); + } + } + + private void printDealerEndingStatus(Dealer dealer) { + printDealerCards(dealer.getCards()); + printScore(dealer.calculateScore()); + System.out.println(); + } + + private void printPlayerCards(String name, List cards) { + System.out.print(name + "카드: "); + printCards(cards); + } + + private void printPlayersEndingStatus(Players players) { + for (Player player : players.getPlayers()) { + printPlayerCards(player.getName(), player.getCards()); + printScore(player.calculateScore()); + System.out.println(); + } + } + + public void printPlayerCards(Player player) { + printPlayerCards(player.getName(), player.getCards()); + System.out.println(); + } + + private void printCards(List cards) { + String printingFormat = cards.stream() + .map(this::toPrintedFormat) + .collect(Collectors.joining(", ")); + System.out.print(printingFormat); + } + + private void printScore(int score) { + System.out.print(" - 결과 : " + score); + } + + private String toPrintedFormat(Players players) { + return players.getPlayers().stream() + .map(Player::getName) + .collect(Collectors.joining(", ")); + } + + private String toPrintedFormat(Card card) { + return VALUE_NAME.get(card.getValue()) + SHAPE_NAME.get(card.getShape()); + } + + public void printDealerDraw() { + System.out.println(); + System.out.print("딜러는 16이하라 한장의 카드를 더 받았습니다."); + } + + public void printMatchResult(Profit dealerProfit, Map playersProfit) { + System.out.println("## 최종 승패"); + printDealerResult(dealerProfit); + for (Entry profitEntry : playersProfit.entrySet()) { + printPlayerResult(profitEntry.getKey(), profitEntry.getValue()); + } + } + + private void printDealerResult(Profit dealerProfit) { + System.out.println("딜러: " + dealerProfit.toInt()); + } + + private void printPlayerResult(Player player, Profit profit) { + String name = player.getName(); + int profitValue = profit.toInt(); + System.out.println(name + ": " + profitValue); + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/test/java/blackjack/domain/card/CardTest.java b/src/test/java/blackjack/domain/card/CardTest.java new file mode 100644 index 00000000000..3790c5041e7 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,54 @@ +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.Nested; +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.EnumSource; + +public class CardTest { + + @DisplayName("카드 값에 따라 점수를 확인할 수 있다") + @Nested + class CardScoreTest { + + @DisplayName("숫자 카드는 카드의 숫자 값을 점수로 가진다") + @ParameterizedTest + @CsvSource({"TWO, 2", "TEN, 10"}) + void scoreTest_whenNumberCard(Value value, int expected) { + Card card = new Card(value, Shape.CLOVER); + + assertAll( + () -> assertThat(card.getMinScore()).isEqualTo(expected), + () -> assertThat(card.getMaxScore()).isEqualTo(expected) + ); + } + + @DisplayName("Jack, Queen, King 카드는 모두 10점을 가진다.") + @ParameterizedTest + @EnumSource(names = {"JACK", "QUEEN", "KING"}) + void scoreTest_whenJQK_Card(Value value) { + Card card = new Card(value, Shape.HEART); + + assertAll( + () -> assertThat(card.getMinScore()).isEqualTo(10), + () -> assertThat(card.getMaxScore()).isEqualTo(10) + ); + } + + @DisplayName("Ace 카드는 1, 또는 11점을 가진다.") + @Test + void scoreTest_whenAceCard() { + Card card = new Card(Value.ACE, Shape.HEART); + + assertAll( + () -> assertThat(card.getMinScore()).isEqualTo(1), + () -> assertThat(card.getMaxScore()).isEqualTo(11) + ); + } + } +} diff --git a/src/test/java/blackjack/domain/card/DeckTest.java b/src/test/java/blackjack/domain/card/DeckTest.java new file mode 100644 index 00000000000..d28ee2e53ea --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,35 @@ +package blackjack.domain.card; + +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; + +class DeckTest { + + @DisplayName("카드 한 장을 뽑을 수 있다") + @Test + void drawTest() { + Deck deck = Deck.createShuffledDeck(); + + assertThat(deck.draw()).isNotNull(); + } + + @DisplayName("카드는 최대 52장만 뽑을 수 있다.") + @Test + void drawTest_drawTooManyTimes_throwException() { + Deck deck = Deck.createShuffledDeck(); + drawRepeat(deck, 52); + + assertThatThrownBy(() -> deck.draw()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("카드를 더 이상 뽑을 수 없습니다."); + } + + private void drawRepeat(Deck deck, int times) { + for (int i = 0; i < times; i++) { + deck.draw(); + } + } +} 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..5bc970c5856 --- /dev/null +++ b/src/test/java/blackjack/domain/card/HandTest.java @@ -0,0 +1,128 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.fixture.CardsFixture; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HandTest { + + @DisplayName("점수를 계산할 수 있다.") + @ParameterizedTest + @MethodSource("cardsAndScore") + void calculateScoreTest(List cards, int expected) { + Hand hand = new Hand(cards); + + assertThat(hand.calculateScore()).isEqualTo(expected); + } + + static Stream cardsAndScore() { + return Stream.of(Arguments.of(CardsFixture.BLACKJACK, 21), + Arguments.of(CardsFixture.TWO_ACE, 12), + Arguments.of(CardsFixture.CARDS_SCORE_16, 16)); + } + + @DisplayName("카드를 한 장 뽑는다") + @Test + void addTest() { + Hand hand = new Hand(CardsFixture.CARDS_SCORE_16); + + Card additionalCard = new Card(Value.ACE, Shape.HEART); + Hand actual = hand.add(additionalCard); + + assertThat(actual.getCards()) + .containsAll(CardsFixture.CARDS_SCORE_16) + .contains(additionalCard) + .hasSize(CardsFixture.CARDS_SCORE_16.size() + 1); + } + + @DisplayName("카드의 버스트 상태를 알 수 있다.") + @Nested + class BustTest { + + @DisplayName("21점이 넘으면 버스트이다.") + @Test + void whenBusted_returnTrue() { + Hand bustedHand = new Hand(CardsFixture.CARDS_SCORE_22); + + assertThat(bustedHand.isBusted()).isTrue(); + } + + @DisplayName("21점 이하 점수는 버스트가 아니다.") + @Test + void whenNotBusted_returnFalse() { + Hand hand = new Hand(CardsFixture.CARDS_SCORE_21); + + assertThat(hand.isBusted()).isFalse(); + } + } + + @DisplayName("카드의 블랙잭 상태를 판단할 수 있다.") + @Nested + class BlackjackTest { + + @DisplayName("21점이면서 2장의 카드라면 블랙잭이다.") + @Test + void whenBlackjack_returnTrue() { + Hand blackjackHand = new Hand(CardsFixture.BLACKJACK); + + assertThat(blackjackHand.isBlackjack()).isTrue(); + } + + @DisplayName("21점 미만 점수는 블랙잭이 아니다.") + @Test + void whenUnderScore_returnFalse() { + Hand blackjackHand = new Hand(List.of( + new Card(Value.KING, Shape.HEART), + new Card(Value.QUEEN, Shape.HEART) + )); + + assertThat(blackjackHand.isBlackjack()).isFalse(); + } + + @DisplayName("21점이지만 3장의 카드라면 블랙잭이 아니다.") + @Test + void whenOverSize_returnFalse() { + Hand blackjackHand = new Hand(CardsFixture.CARDS_SCORE_21); + + assertThat(blackjackHand.isBlackjack()).isFalse(); + } + + @DisplayName("21점 초과 점수는 블랙잭이 아니다.") + @Test + void whenOverScore_returnFalse() { + Hand blackjackHand = new Hand(CardsFixture.CARDS_SCORE_22); + + assertThat(blackjackHand.isBlackjack()).isFalse(); + } + } + + @DisplayName("빈 손패인지 확인할 수 있다.") + @Nested + class EmptyHandTest { + + @DisplayName("아무 카드도 존재하지 않으면 빈 손패이다.") + @Test + void whenEmptyHand_returnTrue() { + Hand emptyHand = new Hand(Collections.emptyList()); + + assertThat(emptyHand.isEmpty()).isTrue(); + } + + @DisplayName("카드가 한 장이라도 존재하면 빈 손패가 아니다.") + @Test + void whenCardExists_returnFalse() { + Hand emptyHand = new Hand(List.of(new Card(Value.ACE, Shape.HEART))); + + assertThat(emptyHand.isEmpty()).isFalse(); + } + } +} diff --git a/src/test/java/blackjack/domain/fixture/CardsFixture.java b/src/test/java/blackjack/domain/fixture/CardsFixture.java new file mode 100644 index 00000000000..8005fbde325 --- /dev/null +++ b/src/test/java/blackjack/domain/fixture/CardsFixture.java @@ -0,0 +1,44 @@ +package blackjack.domain.fixture; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import java.util.List; + +public class CardsFixture { + public static final List CARDS_SCORE_4 = List.of( + new Card(Value.TWO, Shape.HEART), + new Card(Value.TWO, Shape.SPADE) + ); + public static final List TWO_ACE = List.of( + new Card(Value.ACE, Shape.HEART), + new Card(Value.ACE, Shape.SPADE) + ); + public static final List CARDS_SCORE_16 = List.of( + new Card(Value.JACK, Shape.HEART), + new Card(Value.SIX, Shape.HEART) + ); + public static final List CARDS_SCORE_17 = List.of( + new Card(Value.JACK, Shape.HEART), + new Card(Value.SEVEN, Shape.HEART) + ); + public static final List CARDS_SCORE_21 = List.of( + new Card(Value.JACK, Shape.HEART), + new Card(Value.EIGHT, Shape.HEART), + new Card(Value.THREE, Shape.HEART) + ); + public static final List BLACKJACK = List.of( + new Card(Value.ACE, Shape.HEART), + new Card(Value.KING, Shape.HEART) + ); + public static final List CARDS_SCORE_22 = List.of( + new Card(Value.JACK, Shape.HEART), + new Card(Value.SEVEN, Shape.HEART), + new Card(Value.FIVE, Shape.HEART) + ); + public static final List BUSTED = List.of( + new Card(Value.KING, Shape.DIAMOND), + new Card(Value.QUEEN, Shape.DIAMOND), + new Card(Value.JACK, Shape.DIAMOND) + ); +} diff --git a/src/test/java/blackjack/domain/handrank/BlackjackTest.java b/src/test/java/blackjack/domain/handrank/BlackjackTest.java new file mode 100644 index 00000000000..0549722fd97 --- /dev/null +++ b/src/test/java/blackjack/domain/handrank/BlackjackTest.java @@ -0,0 +1,51 @@ +package blackjack.domain.handrank; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class BlackjackTest { + + private final HankRank BLACKJACK = new Blackjack(); + + @DisplayName("플레이어, 딜러 모두 블랙잭인 경우 비긴다.") + @Test + void matchTest_whenPlayerAndDealerBlackjack_matchDraw() { + HankRank dealerRank = BLACKJACK; + HankRank playerRank = BLACKJACK; + + assertThat(dealerRank.matchAtDealer(playerRank)).isEqualTo(SingleMatchResult.DRAW); + } + + @DisplayName("딜러만 블랙잭인 경우, 딜러가 이긴다.") + @ParameterizedTest + @MethodSource("notBlackjack") + void matchTest_whenOnlyDealerBlackjack_dealerWin(HankRank hankRank) { + HankRank dealerRank = BLACKJACK; + HankRank playerRank = hankRank; + + assertThat(dealerRank.matchAtDealer(playerRank)).isEqualTo(SingleMatchResult.DEALER_WIN); + } + + static Stream notBlackjack() { + return Stream.of(new Stand(12), new Stand(20), new Bust(22)); + } + + @DisplayName("해당 핸드 랭크는 블랙잭이다.") + @Test + void isBlackjackTest() { + + assertThat(BLACKJACK.isBlackjack()).isTrue(); + } + + @DisplayName("해당 핸드 랭크는 버스트가 아니다.") + @Test + void isBustTest() { + + assertThat(BLACKJACK.isBust()).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/handrank/BustTest.java b/src/test/java/blackjack/domain/handrank/BustTest.java new file mode 100644 index 00000000000..9a16a4dd5e4 --- /dev/null +++ b/src/test/java/blackjack/domain/handrank/BustTest.java @@ -0,0 +1,60 @@ +package blackjack.domain.handrank; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class BustTest { + + private static final HankRank BUST = new Bust(22); + + @DisplayName("플레이어, 딜러 모두 버스트인 경우 딜러가 이긴다.") + @Test + void matchTest_whenPlayerAndDealerBust_DealerWin() { + HankRank dealerRank = BUST; + HankRank playerRank = BUST; + + assertThat(dealerRank.matchAtDealer(playerRank)).isEqualTo(SingleMatchResult.DEALER_WIN); + } + + @DisplayName("딜러만 버스트인 경우, 플레이어가 이긴다.") + @ParameterizedTest + @MethodSource("normalRank") + void matchTest_whenOnlyDealerBlackjack_PlayerWin(HankRank hankRank) { + HankRank dealerRank = BUST; + HankRank playerRank = hankRank; + + assertThat(dealerRank.matchAtDealer(playerRank)).isEqualTo(SingleMatchResult.PLAYER_WIN); + } + + static Stream normalRank() { + return Stream.of(new Stand(12), new Stand(20), new Stand(21)); + } + + @DisplayName("딜러만 버스트이고 플레이어가 블랙잭인 경우, 플레이어 블랙잭으로 승리한다.") + @Test + void matchTest_whenOnlyDealerBlackjack_PlayerBlackjackWin() { + HankRank dealerRank = BUST; + HankRank playerRank = new Blackjack(); + + assertThat(dealerRank.matchAtDealer(playerRank)).isEqualTo(SingleMatchResult.PLAYER_BLACKJACK); + } + + @DisplayName("해당 핸드 랭크는 블랙잭이 아니다.") + @Test + void isBlackjackTest() { + + assertThat(BUST.isBlackjack()).isFalse(); + } + + @DisplayName("해당 핸드 랭크는 버스트이다.") + @Test + void isBustTest() { + + assertThat(BUST.isBust()).isTrue(); + } +} diff --git a/src/test/java/blackjack/domain/handrank/HandRankFactoryTest.java b/src/test/java/blackjack/domain/handrank/HandRankFactoryTest.java new file mode 100644 index 00000000000..29439a907c8 --- /dev/null +++ b/src/test/java/blackjack/domain/handrank/HandRankFactoryTest.java @@ -0,0 +1,56 @@ +package blackjack.domain.handrank; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.fixture.CardsFixture; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class HandRankFactoryTest { + + @DisplayName("손패가 블랙잭인 경우, 블랙잭 객체를 반환한다.") + @Test + void fromTest_whenBlackjack() { + Hand blackjack = new Hand(CardsFixture.BLACKJACK); + + HankRank actual = HandRankFactory.from(blackjack); + + assertThat(actual).isExactlyInstanceOf(Blackjack.class); + } + + @DisplayName("손패가 버스트가 된 경우, 버스트 객체를 반환한다.") + @ParameterizedTest + @MethodSource("bustCards") + void fromTest_whenBusted(List bust) { + Hand blackjack = new Hand(bust); + + HankRank actual = HandRankFactory.from(blackjack); + + assertThat(actual).isExactlyInstanceOf(Bust.class); + } + + static Stream> bustCards() { + return Stream.of(CardsFixture.BUSTED, CardsFixture.CARDS_SCORE_22); + } + + @DisplayName("손패가 버스트, 블랙잭이 아닌 경우, 일반 랭크 객체를 반환한다.") + @ParameterizedTest + @MethodSource("normalCards") + void fromTest_whenNotBlackjackAndNotBust(List cards) { + Hand blackjack = new Hand(cards); + + HankRank actual = HandRankFactory.from(blackjack); + + assertThat(actual).isExactlyInstanceOf(Stand.class); + } + + static Stream> normalCards() { + return Stream.of(CardsFixture.CARDS_SCORE_4, CardsFixture.CARDS_SCORE_17, CardsFixture.CARDS_SCORE_16); + } +} diff --git a/src/test/java/blackjack/domain/handrank/SingleMatchResultTest.java b/src/test/java/blackjack/domain/handrank/SingleMatchResultTest.java new file mode 100644 index 00000000000..c6c250aa1fb --- /dev/null +++ b/src/test/java/blackjack/domain/handrank/SingleMatchResultTest.java @@ -0,0 +1,28 @@ +package blackjack.domain.handrank; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.money.BetAmount; +import blackjack.domain.money.Profit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SingleMatchResultTest { + + @DisplayName("승패 결과에 따라, 플레이어의 이익을 반환할 수 있다.") + @ParameterizedTest + @CsvSource({ + "1000, PLAYER_BLACKJACK, 1500", + "1000, PLAYER_WIN, 1000", + "1000, DRAW, 0", + "1000, DEALER_WIN, -1000" + }) + void calculatePlayerProfitTest(int amount, SingleMatchResult result, int expected) { + BetAmount betAmount = new BetAmount(amount); + + Profit actual = result.calculatePlayerProfit(betAmount); + + assertThat(actual.toInt()).isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/handrank/StandTest.java b/src/test/java/blackjack/domain/handrank/StandTest.java new file mode 100644 index 00000000000..4b4d3eb62db --- /dev/null +++ b/src/test/java/blackjack/domain/handrank/StandTest.java @@ -0,0 +1,82 @@ +package blackjack.domain.handrank; + +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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class StandTest { + + private final HankRank STAND = new Stand(20); + + @DisplayName("딜러가 21점 이하 점수의 일반적인 경우") + @Nested + class MatchTest { + @DisplayName("플레이어가 블랙잭인 경우, 플레이어가 블랙잭으로 승리한다.") + @Test + void whenOnlyPlayerBlackjack_playerBlackjack() { + HankRank dealer = new Stand(21); + HankRank player = new Blackjack(); + + assertThat(dealer.matchAtDealer(player)).isEqualTo(SingleMatchResult.PLAYER_BLACKJACK); + } + + @DisplayName("플레이어가 버스트인 경우, 딜러가 승리한다.") + @Test + void whenOnlyPlayerBust_DealerWin() { + HankRank dealer = STAND; + HankRank player = new Bust(22); + + assertThat(dealer.matchAtDealer(player)).isEqualTo(SingleMatchResult.DEALER_WIN); + } + + @DisplayName("플레이어가 딜러보다 점수가 높을 경우, 플레이러가 승리한다.") + @ParameterizedTest + @CsvSource({"20, 21", "17, 18", "17, 21"}) + void whenPlayerScoreIsMoreThanDealerScore_PlayerWin(int dealerScore, int playerScore) { + HankRank dealer = new Stand(dealerScore); + HankRank player = new Stand(playerScore); + + assertThat(dealer.matchAtDealer(player)).isEqualTo(SingleMatchResult.PLAYER_WIN); + } + + @DisplayName("플레이어와 딜러의 점수가 같을 경우, 비긴다.") + @ParameterizedTest + @ValueSource(ints = {17, 18, 21}) + void whenPlayerScoreIsEqualToDealerScore_Draw(int sameScore) { + HankRank dealer = new Stand(sameScore); + HankRank player = new Stand(sameScore); + + assertThat(dealer.matchAtDealer(player)).isEqualTo(SingleMatchResult.DRAW); + } + + @DisplayName("플레이어가 딜러보다 점수가 높을 경우, 플레이러가 승리한다.") + @ParameterizedTest + @CsvSource({"21, 20", "17, 4", "17, 16"}) + void whenPlayerScoreIsLessThanDealerScore_Dealer(int dealerScore, int playerScore) { + HankRank dealer = new Stand(dealerScore); + HankRank player = new Stand(playerScore); + + assertThat(dealer.matchAtDealer(player)).isEqualTo(SingleMatchResult.DEALER_WIN); + } + } + + + @DisplayName("해당 핸드 랭크는 블랙잭이 아니다.") + @Test + void isBlackjackTest() { + + assertThat(STAND.isBlackjack()).isFalse(); + } + + @DisplayName("해당 핸드 랭크는 버스트가 아니다.") + @Test + void isBustTest() { + + assertThat(STAND.isBust()).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/money/BetAmountTest.java b/src/test/java/blackjack/domain/money/BetAmountTest.java new file mode 100644 index 00000000000..e5c31068379 --- /dev/null +++ b/src/test/java/blackjack/domain/money/BetAmountTest.java @@ -0,0 +1,20 @@ +package blackjack.domain.money; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BetAmountTest { + + @DisplayName("배팅 금액은 0 또는 음수가 될 수 없다") + @ParameterizedTest + @ValueSource(ints = {0, -1, -1000}) + void validateTest_whenValueIsNotPositive_throwException(int value) { + + assertThatThrownBy(() -> new BetAmount(value)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("배팅 금액은 양수이어야 합니다."); + } +} diff --git a/src/test/java/blackjack/domain/money/ProfitTest.java b/src/test/java/blackjack/domain/money/ProfitTest.java new file mode 100644 index 00000000000..8159d971675 --- /dev/null +++ b/src/test/java/blackjack/domain/money/ProfitTest.java @@ -0,0 +1,50 @@ +package blackjack.domain.money; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ProfitTest { + + @DisplayName("주어진 배팅 금액에 특정 배수만큼 얻는 이익을 계산할 수 있다.") + @ParameterizedTest + @CsvSource({ + "1000, 2, 2000", + "20, 1.5, 30", + "1111, 1.5, 1666", + "100, -1, -100", + "3333, 0, 0" + }) + void createTest_winMoneyWithMultiplier(int amount, double multiplier, int expected) { + BetAmount betAmount = new BetAmount(amount); + + Profit profit = Profit.of(betAmount, multiplier); + + assertThat(profit.toInt()).isEqualTo(expected); + } + + @DisplayName("두 이익을 더하여 계산할 수 있다.") + @ParameterizedTest + @CsvSource({"20, -10", "10, -20", "-20, -10", "20, 10"}) + void addTest(int value, int addedValue) { + Profit profit = new Profit(value); + Profit addedProfit = new Profit(addedValue); + + Profit actual = profit.add(addedProfit); + + assertThat(actual.toInt()).isEqualTo(value + addedValue); + } + + @DisplayName("얻은 만큼 잃은 이익을, 잃은 만큼 얻은 이익을 구할 수 있다.") + @ParameterizedTest + @CsvSource({"20, -20", "-10, 10", "-1, 1", "0, 0"}) + void reverseTest(int value, int expected) { + Profit profit = new Profit(value); + + Profit actual = profit.reverse(); + + assertThat(actual.toInt()).isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/participant/DealerTest.java b/src/test/java/blackjack/domain/participant/DealerTest.java new file mode 100644 index 00000000000..b2e962fff19 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,136 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import blackjack.domain.fixture.CardsFixture; +import blackjack.domain.money.BetAmount; +import blackjack.domain.money.Profit; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class DealerTest { + + @DisplayName("카드의 총 점수가 16을 넘지 않으면, 카드를 더 뽑을 수 있다") + @Test + void isDrawableTest_whenScoreIsUnderBound_returnTrue() { + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_16); + + assertThat(dealer.isDrawable()).isTrue(); + } + + @DisplayName("카드의 총 점수가 17을 넘으면, 카드를 더 뽑을 수 없다") + @Test + void isDrawableTest_whenScoreIsOverBound_returnFalse() { + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_17); + + assertThat(dealer.isDrawable()).isFalse(); + } + + @DisplayName("점수를 계산할 수 있다.") + @ParameterizedTest + @MethodSource("cardsAndScore") + void calculateScoreTest(List cards, int expected) { + Dealer dealer = new Dealer(cards); + + assertThat(dealer.calculateScore()).isEqualTo(expected); + } + + static Stream cardsAndScore() { + return Stream.of( + Arguments.of(CardsFixture.BLACKJACK, 21), + Arguments.of(CardsFixture.TWO_ACE, 12), + Arguments.of(CardsFixture.CARDS_SCORE_16, 16) + ); + } + + @DisplayName("게임을 시작할 때는 카드를 두 장 뽑는다.") + @Test + void drawStartCardsTest() { + Dealer dealer = new Dealer(); + Deck deck = Deck.createShuffledDeck(); + + dealer.drawStartCards(deck); + + assertThat(dealer.getCards()).hasSize(2); + } + + @DisplayName("이미 카드를 가지고 있는 경우, 시작 카드를 뽑을 수 없다.") + @Test + void drawStartCardsTest_whenAlreadyStarted_throwException() { + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_16); + Deck deck = Deck.createShuffledDeck(); + + assertThatThrownBy(() -> dealer.drawStartCards(deck)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 시작 카드를 뽑았습니다."); + } + + @DisplayName("카드의 총 점수가 16을 넘지 않으면, 카드를 한 장 뽑는다") + @Test + void addTest_whenScoreIsUnderBound() { + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_16); + Card additionalCard = new Card(Value.ACE, Shape.HEART); + + dealer.add(additionalCard); + + assertThat(dealer.getCards()) + .containsAll(CardsFixture.CARDS_SCORE_16) + .contains(additionalCard) + .hasSize(3); + } + + @DisplayName("카드의 총 점수가 16을 넘으면, 카드를 뽑을 때 예외가 발생한다.") + @Test + void addTest_whenScoreIsOverBound_throwException() { + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_17); + Card card = new Card(Value.ACE, Shape.HEART); + + assertThatThrownBy(() -> dealer.add(card)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("더 이상 카드를 추가할 수 없습니다."); + } + + @DisplayName("플레이어들의 이익을 계산할 수 있다.") + @Test + void calculatePlayersProfitTest() { + Player blackjackPlayer = new Player(CardsFixture.BLACKJACK, new Name("black"), new BetAmount(1_000)); + Player winPlayer = new Player(CardsFixture.CARDS_SCORE_21, new Name("win"), new BetAmount(2_000)); + Player losePlayer = new Player(CardsFixture.CARDS_SCORE_16, new Name("lose"), new BetAmount(3_000)); + Players players = new Players(List.of(blackjackPlayer, winPlayer, losePlayer)); + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_17); + + Map matchResult = dealer.calculatePlayersProfit(players); + + assertThat(matchResult).containsExactly( + Map.entry(blackjackPlayer, new Profit(1_500)), + Map.entry(winPlayer, new Profit(2_000)), + Map.entry(losePlayer, new Profit(-3_000)) + ); + } + + @DisplayName("딜러의 이익을 계산할 수 있다.") + @Test + void calculateDealerProfitTest() { + Player blackjackPlayer = new Player(CardsFixture.BLACKJACK, new Name("black"), new BetAmount(1_000)); + Player winPlayer = new Player(CardsFixture.CARDS_SCORE_21, new Name("win"), new BetAmount(2_000)); + Player losePlayer = new Player(CardsFixture.CARDS_SCORE_16, new Name("lose"), new BetAmount(3_000)); + Players players = new Players(List.of(blackjackPlayer, winPlayer, losePlayer)); + Dealer dealer = new Dealer(CardsFixture.CARDS_SCORE_17); + int expected = -1_500 - 2_000 + 3_000; + + Profit dealerProfit = dealer.calculateDealerProfit(players); + + assertThat(dealerProfit.toInt()).isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/participant/NameTest.java b/src/test/java/blackjack/domain/participant/NameTest.java new file mode 100644 index 00000000000..486850bbef3 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/NameTest.java @@ -0,0 +1,32 @@ +package blackjack.domain.participant; + +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + + @DisplayName("이름은 적어도 한 글자를 가져야 한다.") + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "\n"}) + void validateTest_whenNameIsEmpty_throwException(String name) { + + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 적어도 한 글자 이상을 포함해야 합니다."); + } + + @DisplayName("이름의 앞 뒤 공백을 제거해준다.") + @ParameterizedTest + @ValueSource(strings = {" pobi", "pobi ", " pobi ", "\npobi\t"}) + void validateTest_nameHasStrippedName(String input) { + Name name = new Name(input); + + assertThat(name.getName()).isEqualTo("pobi"); + } +} diff --git a/src/test/java/blackjack/domain/participant/PlayerTest.java b/src/test/java/blackjack/domain/participant/PlayerTest.java new file mode 100644 index 00000000000..ff78164aa33 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,125 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import blackjack.domain.fixture.CardsFixture; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PlayerTest { + + private static final Name DEFAULT_NAME = new Name("name"); + + @DisplayName("점수를 계산할 수 있다.") + @ParameterizedTest + @MethodSource("cardsAndScore") + void calculateScoreTest(List cards, int expected) { + Player player = new Player(cards, DEFAULT_NAME); + + assertThat(player.calculateScore()).isEqualTo(expected); + } + + static Stream cardsAndScore() { + return Stream.of( + Arguments.of(CardsFixture.BLACKJACK, 21), + Arguments.of(CardsFixture.TWO_ACE, 12), + Arguments.of(CardsFixture.CARDS_SCORE_16, 16) + ); + } + + @DisplayName("ACE가 11로 계산되었을 때 버스트가 되지 않는다면, ACE를 그대로 계산한다.") + @Test + void calculateScoreTest_usingAceMaxScoreCase() { + Player player = new Player(List.of( + new Card(Value.ACE, Shape.HEART), + new Card(Value.TWO, Shape.HEART) + ), DEFAULT_NAME); + + assertThat(player.calculateScore()).isEqualTo(13); + } + + @DisplayName("ACE가 11로 계산되었을 때 버스트가 된다면, ACE를 1로 계산한다.") + @Test + void calculateScoreTest_usingAceMinScoreCase() { + Player player = new Player(List.of( + new Card(Value.ACE, Shape.HEART), + new Card(Value.KING, Shape.HEART), + new Card(Value.TWO, Shape.HEART) + ), DEFAULT_NAME); + + assertThat(player.calculateScore()).isEqualTo(13); + } + + @DisplayName("카드의 총 점수가 21을 넘지 않으면, 카드를 더 뽑을 수 있다") + @Test + void isDrawableTest_whenScoreIsUnderBound_returnTrue() { + Player player = new Player(CardsFixture.CARDS_SCORE_21, DEFAULT_NAME); + + assertThat(player.isDrawable()).isTrue(); + } + + @DisplayName("카드의 총 점수가 21을 넘으면, 카드를 더 뽑을 수 없다") + @Test + void isDrawableTest_whenScoreIsOverBound_returnFalse() { + Player player = new Player(CardsFixture.CARDS_SCORE_22, DEFAULT_NAME); + + assertThat(player.isDrawable()).isFalse(); + } + + @DisplayName("게임을 시작할 때는 카드를 두 장 뽑는다.") + @Test + void drawStartCardsTest() { + Player player = Player.from("name", 1000); + Deck deck = Deck.createShuffledDeck(); + + player.drawStartCards(deck); + + assertThat(player.getCards()).hasSize(2); + } + + @DisplayName("이미 카드를 가지고 있는 경우, 시작 카드를 뽑을 수 없다.") + @Test + void drawStartCardsTest_whenAlreadyStarted_throwException() { + Player player = new Player(CardsFixture.CARDS_SCORE_16, DEFAULT_NAME); + Deck deck = Deck.createShuffledDeck(); + + assertThatThrownBy(() -> player.drawStartCards(deck)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 시작 카드를 뽑았습니다."); + } + + @DisplayName("카드의 총 점수가 21을 넘지 않으면, 카드를 한 장 뽑는다") + @Test + void addTest_whenScoreIsUnderBound() { + Player player = new Player(CardsFixture.CARDS_SCORE_21, DEFAULT_NAME); + + Card additionalCard = new Card(Value.ACE, Shape.HEART); + player.add(additionalCard); + + assertThat(player.getCards()) + .containsAll(CardsFixture.CARDS_SCORE_21) + .contains(additionalCard) + .hasSize(4); + } + + @DisplayName("카드의 총 점수가 21을 넘으면, 카드를 뽑을 때 예외가 발생한다.") + @Test + void addTest_whenScoreIsOverBound_throwException() { + Player player = new Player(CardsFixture.CARDS_SCORE_22, DEFAULT_NAME); + Card card = new Card(Value.ACE, Shape.HEART); + + assertThatThrownBy(() -> player.add(card)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("더 이상 카드를 추가할 수 없습니다."); + } +} diff --git a/src/test/java/blackjack/domain/participant/PlayersTest.java b/src/test/java/blackjack/domain/participant/PlayersTest.java new file mode 100644 index 00000000000..543e576e3fa --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayersTest.java @@ -0,0 +1,62 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.domain.card.Deck; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + private static final Player PLAYER1 = Player.from("1", 1000); + private static final Player PLAYER2 = Player.from("2", 2000); + private static final Player PLAYER3 = Player.from("3", 3000); + private static final Player PLAYER4 = Player.from("4", 4000); + private static final Player PLAYER5 = Player.from("5", 5000); + + @DisplayName("최소 한 명 이상의 플레이어가 존재해야 한다.") + @Test + void validateTest_countOfPlayersIsZero_throwException() { + assertThatThrownBy(() -> new Players(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("최소 한 명의 플레이어가 있어야 합니다."); + } + + @DisplayName("4명 이상의 플레이어를 가지면 예외가 발생한다.") + @Test + void validateTest_tooManyPlayers_throwException() { + List manyPlayers = List.of(PLAYER1, PLAYER2, PLAYER3, PLAYER4, PLAYER5); + + assertThatThrownBy(() -> new Players(manyPlayers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("최대 4명의 플레이어만 참여 가능합니다."); + } + + @DisplayName("중복된 이름을 사용하면, 예외가 발생한다.") + @Test + void validateTest_whenNameIsDuplicated_throwException() { + List duplicatedPlayers = List.of(PLAYER1, PLAYER1, PLAYER2); + + assertThatThrownBy(() -> new Players(duplicatedPlayers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("중복된 이름을 사용할 수 없습니다."); + } + + @DisplayName("게임을 시작할 때 모든 플레이어들은 카드를 두 장 뽑는다.") + @Test + void drawStartCardsTest() { + Players players = new Players(List.of(PLAYER1, PLAYER2)); + Deck deck = Deck.createShuffledDeck(); + + players.drawStartCards(deck); + + assertAll( + () -> assertThat(PLAYER1.getCards()).hasSize(2), + () -> assertThat(PLAYER2.getCards()).hasSize(2) + ); + } +}