diff --git a/README.md b/README.md index 3bcfc257847..9f8ffb20dd2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,39 @@ # java-blackjack 블랙잭 게임 미션 저장소 -## 우아한테크코스 코드리뷰 -* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file +## 기능 목록 +* [X] 게임에 참여할 사람의 이름을 입력받는다. + * [X] 참여자의 이름은 1 ~ 10자로 구성된다. + * [X] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. + * [X] 참여자의 이름은 comma(,) 단위로 구분한다. + * [X] 참여자의 수는 2 ~ 8명이다. +* [X] 각 참여자의 베팅 금액을 입력받는다. + * [X] 베팅 금액의 최소 단위는 100으로 제한한다. +* [X] 딜러와 각 참여자에게 카드를 두 장씩 분배한다. +* [X] 각 참여자에게 나누어 준 카드를 출력한다. + * [X] 딜러는 첫 번째 카드를 제외하고 한 장의 카드만 공개한다. + * [X] 각 참여자는 두 장의 카드를 공개한다. +* 딜러가 블랙잭인지 여부를 판별하고, 블랙잭이면 결과를 출력한다. + * [X] 참여자가 블랙잭이라면 무승부이다. + * [X] 참여자가 블랙잭이 아니라면 딜러의 승리이다. +* 참여자 히트 + * [X] 참여자가 블랙잭, 버스트가 아니라면 추가적으로 카드를 히트할지 여부를 입력받는다. + * [X] 참여자가 스테이하면 다음 사람 턴으로 넘어간다. + * [X] 참여자가 히트하면 카드 발급 후 카드를 출력한다. +* 딜러 히트 + * [X] 딜러는 가진 패의 합이 16 이하라면 반드시 히트해야 한다. +* [X] 딜러와 참여자가 받은 모든 패를 공개하고, 결과를 출력한다. +* [X] 각 참여자의 최종 수익을 출력한다. + +## 게임 조건 +* 두 장의 패를 뽑아 합이 21인 경우 블랙잭이다. +* 카드의 합이 21을 초과하는 경우 버스트이다. +* 각 ACE는 1 또는 11로 계산할 수 있다. +* 참여자가 버스트가 되면 딜러의 버스트 여부와 상관없이 참여자가 패배한다. + +### 수익 계산 +* 무승부인 경우 베팅한 금액을 돌려받아 수익이 없다. +* 참여자가 승리한 경우 + * 블랙잭이 아니라면 베팅 금액만큼 수익을 얻는다. + * 블랙잭이라면 베팅 금액의 1.5배의 수익을 얻는다. +* 참여자가 패배하면 베팅한 모든 금액을 잃는다. diff --git a/src/main/java/BlackjackApplication.java b/src/main/java/BlackjackApplication.java new file mode 100644 index 00000000000..50ab147aac5 --- /dev/null +++ b/src/main/java/BlackjackApplication.java @@ -0,0 +1,39 @@ +import java.util.Scanner; + +import controller.GameController; +import service.DrawService; +import service.PlayerService; +import service.ProfitService; +import view.InputView; +import view.OutputView; + +public class BlackjackApplication { + private final GameController gameController; + + public BlackjackApplication(final GameController gameController) { + this.gameController = gameController; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + InputView inputView = new InputView(scanner); + OutputView outputView = new OutputView(); + PlayerService playerService = new PlayerService(); + DrawService drawService = new DrawService(); + ProfitService profitService = new ProfitService(); + + GameController gameController = new GameController(inputView, outputView, playerService, drawService, + profitService); + BlackjackApplication blackjackApplication = new BlackjackApplication(gameController); + + blackjackApplication.run(); + } + + public void run() { + try { + gameController.run(); + } catch (Exception exception) { + System.err.println(exception.getMessage()); + } + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000000..32871a70465 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,87 @@ +package controller; + +import java.util.ArrayList; +import java.util.List; + +import domain.BettingMoney; +import domain.Name; +import domain.card.CardDeck; +import domain.card.CardDeckFactory; +import domain.participant.Dealer; +import domain.participant.Player; +import domain.result.ParticipantProfit; +import service.DrawService; +import service.PlayerService; +import service.ProfitService; +import view.InputView; +import view.OutputView; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final PlayerService playerService; + private final DrawService drawService; + private final ProfitService profitService; + + public GameController(final InputView inputView, final OutputView outputView, final PlayerService playerService, + final DrawService drawService, final ProfitService profitService) { + this.inputView = inputView; + this.outputView = outputView; + this.playerService = playerService; + this.drawService = drawService; + this.profitService = profitService; + } + + public void run() { + CardDeck cardDeck = CardDeckFactory.createRandomCardDeck(); + Dealer dealer = new Dealer(); + List players = createPlayers(); + + drawCards(cardDeck, dealer, players); + + calculateProfit(dealer, players); + } + + private List createPlayers() { + String namesWithComma = inputView.inputNames(); + List names = Name.fromComma(namesWithComma); + List bettingMonies = new ArrayList<>(); + for (final Name name : names) { + int money = inputView.inputBettingMoney(name); + BettingMoney bettingMoney = new BettingMoney(money); + bettingMonies.add(bettingMoney); + } + return playerService.createPlayers(names, bettingMonies); + } + + private void drawCards(final CardDeck cardDeck, final Dealer dealer, final List players) { + drawService.drawInitialCards(cardDeck, dealer, players); + outputView.printHandsAtFirst(dealer, players); + + for (final Player player : players) { + drawCardToPlayer(cardDeck, player); + } + drawCardToDealer(cardDeck, dealer); + } + + private void calculateProfit(final Dealer dealer, final List players) { + outputView.printHandsAtLast(dealer, players); + List participantProfits = profitService.calculateProfits(dealer, players); + outputView.printProfits(participantProfits); + } + + private void drawCardToDealer(final CardDeck cardDeck, final Dealer dealer) { + while (!dealer.isFinished()) { + drawService.drawDealer(cardDeck, dealer); + outputView.printDealerHitMessage(); + } + } + + private void drawCardToPlayer(final CardDeck cardDeck, final Player player) { + while (!player.isFinished()) { + String decision = inputView.inputPlayerDecision(player); + drawService.drawPlayer(cardDeck, player, decision); + outputView.printHand(player); + } + } +} diff --git a/src/main/java/domain/BettingMoney.java b/src/main/java/domain/BettingMoney.java new file mode 100644 index 00000000000..2a97c2d390a --- /dev/null +++ b/src/main/java/domain/BettingMoney.java @@ -0,0 +1,38 @@ +package domain; + +import java.math.BigDecimal; + +public class BettingMoney { + private static final int MIN_AMOUNT = 100; + private static final int BETTING_UNIT = 100; + + private final BigDecimal amount; + + public BettingMoney(final int amount) { + validate(amount); + this.amount = BigDecimal.valueOf(amount); + } + + private void validate(final int amount) { + validateRange(amount); + validateUnit(amount); + } + + private void validateRange(final int amount) { + if (amount < MIN_AMOUNT) { + throw new IllegalArgumentException("베팅 최소 금액을 충족하지 못했습니다.\n" + + "amount: " + amount); + } + } + + private void validateUnit(final int amount) { + if (amount % BETTING_UNIT != 0) { + throw new IllegalArgumentException("금액의 단위가 올바르지 않습니다.\n" + + "amount: " + amount); + } + } + + public BigDecimal getAmount() { + return amount; + } +} diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java new file mode 100644 index 00000000000..d9ee5a6750e --- /dev/null +++ b/src/main/java/domain/Name.java @@ -0,0 +1,40 @@ +package domain; + +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class Name { + private static final int MAX_LENGTH = 10; + private static final int MIN_LENGTH = 1; + private static final String NAME_DELIMITER = ","; + + private final String name; + + public Name(String name) { + Objects.requireNonNull(name, "이름이 null입니다."); + name = name.trim(); + validateLength(name); + this.name = name; + } + + public static List fromComma(final String names) { + return Stream.of(names.split(NAME_DELIMITER)) + .map(Name::new) + .collect(toList()); + } + + private void validateLength(final String name) { + int length = name.length(); + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new IllegalArgumentException("이름의 길이가 올바르지 않습니다.\n" + + "name: " + name); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 00000000000..28b907cebee --- /dev/null +++ b/src/main/java/domain/card/Card.java @@ -0,0 +1,66 @@ +package domain.card; + +import static java.util.stream.Collectors.toList; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class Card { + private final Face face; + private final Suit suit; + + private Card(final Face face, final Suit suit) { + this.face = face; + this.suit = suit; + } + + public static Card fromFaceAndSuit(final Face face, final Suit suit) { + return CardCache.cache + .stream() + .filter(card -> card.isCardOf(face, suit)) + .findFirst() + .orElseThrow(() -> new CardNotFoundException("카드가 존재하지 않습니다.\n" + + "face: " + face + "\n" + + "suit: " + suit)); + } + + public static List values() { + return Collections.unmodifiableList(CardCache.cache); + } + + public boolean isAce() { + return face.isAce(); + } + + public boolean isCardOf(final Face face, final Suit suit) { + return this.face == face && this.suit == suit; + } + + public Face getFace() { + return face; + } + + public Suit getSuit() { + return suit; + } + + public String alias() { + return suit.alias() + face.alias(); + } + + private static class CardCache { + public static final List cache; + + static { + cache = Stream.of(Face.values()) + .flatMap(CardCache::createByFace) + .collect(toList()); + } + + private static Stream createByFace(final Face face) { + return Stream.of(Suit.values()) + .map(suit -> new Card(face, suit)); + } + } +} diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java new file mode 100644 index 00000000000..b67198cfde1 --- /dev/null +++ b/src/main/java/domain/card/CardDeck.java @@ -0,0 +1,5 @@ +package domain.card; + +public interface CardDeck { + Card pick(); +} diff --git a/src/main/java/domain/card/CardDeckFactory.java b/src/main/java/domain/card/CardDeckFactory.java new file mode 100644 index 00000000000..7d96f10703b --- /dev/null +++ b/src/main/java/domain/card/CardDeckFactory.java @@ -0,0 +1,11 @@ +package domain.card; + +public class CardDeckFactory { + private CardDeckFactory() { + throw new AssertionError(); + } + + public static CardDeck createRandomCardDeck() { + return new RandomCardDeck(Card.values()); + } +} diff --git a/src/main/java/domain/card/CardNotFoundException.java b/src/main/java/domain/card/CardNotFoundException.java new file mode 100644 index 00000000000..cd5bcd4a65c --- /dev/null +++ b/src/main/java/domain/card/CardNotFoundException.java @@ -0,0 +1,7 @@ +package domain.card; + +public class CardNotFoundException extends RuntimeException { + public CardNotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/domain/card/EmptyCardDeckException.java b/src/main/java/domain/card/EmptyCardDeckException.java new file mode 100644 index 00000000000..370477b8100 --- /dev/null +++ b/src/main/java/domain/card/EmptyCardDeckException.java @@ -0,0 +1,7 @@ +package domain.card; + +public class EmptyCardDeckException extends RuntimeException { + public EmptyCardDeckException(final String message) { + super(message); + } +} diff --git a/src/main/java/domain/card/Face.java b/src/main/java/domain/card/Face.java new file mode 100644 index 00000000000..cf6ccff8fbd --- /dev/null +++ b/src/main/java/domain/card/Face.java @@ -0,0 +1,42 @@ +package domain.card; + +public enum Face { + ACE(1, "A"), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10, "J"), + QUEEN(10, "Q"), + KING(10, "K"); + + private final int score; + private final String alias; + + Face(final int score) { + this.score = score; + this.alias = String.valueOf(score); + } + + Face(final int score, final String alias) { + this.score = score; + this.alias = alias; + } + + public boolean isAce() { + return this == ACE; + } + + public int getScore() { + return score; + } + + public String alias() { + return alias; + } +} diff --git a/src/main/java/domain/card/RandomCardDeck.java b/src/main/java/domain/card/RandomCardDeck.java new file mode 100644 index 00000000000..32dbf6e2750 --- /dev/null +++ b/src/main/java/domain/card/RandomCardDeck.java @@ -0,0 +1,33 @@ +package domain.card; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Stack; + +public class RandomCardDeck implements CardDeck { + private final Stack cards; + + public RandomCardDeck(final List cards) { + validate(cards); + Stack shuffledCards = new Stack<>(); + shuffledCards.addAll(cards); + Collections.shuffle(shuffledCards); + this.cards = shuffledCards; + } + + @Override + public Card pick() { + if (cards.isEmpty()) { + throw new EmptyCardDeckException("카드를 모두 소모하여 더 뽑을 수 없습니다."); + } + return cards.pop(); + } + + private void validate(final List cards) { + Objects.requireNonNull(cards, "카드 리스트가 null입니다."); + if (cards.isEmpty()) { + throw new IllegalArgumentException("카드 리스트의 크기가 0입니다."); + } + } +} diff --git a/src/main/java/domain/card/Suit.java b/src/main/java/domain/card/Suit.java new file mode 100644 index 00000000000..83cc505e3db --- /dev/null +++ b/src/main/java/domain/card/Suit.java @@ -0,0 +1,18 @@ +package domain.card; + +public enum Suit { + SPADE("♠️"), + HEART("❤️"), + DIAMOND("♦️"), + CLUB("♣️"); + + private final String alias; + + Suit(final String alias) { + this.alias = alias; + } + + public String alias() { + return alias; + } +} diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java new file mode 100644 index 00000000000..0a3dd3f19db --- /dev/null +++ b/src/main/java/domain/participant/Dealer.java @@ -0,0 +1,26 @@ +package domain.participant; + +import domain.Name; +import domain.card.CardDeck; +import domain.participant.hand.HandState; + +public class Dealer extends Participant { + private static final Name NAME = new Name("딜러"); + private static final int DEALER_HITTABLE_UPPER_BOUND = 16; + + public Dealer() { + super(NAME); + } + + public Dealer(final HandState hand) { + super(NAME, hand); + } + + @Override + public void hit(final CardDeck cardDeck) { + super.hit(cardDeck); + if (!hand.isFinished() && hand.isOver(DEALER_HITTABLE_UPPER_BOUND)) { + stay(); + } + } +} diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 00000000000..23543c1b7c2 --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,67 @@ +package domain.participant; + +import java.util.Objects; + +import domain.Name; +import domain.card.CardDeck; +import domain.participant.hand.Hand; +import domain.participant.hand.HandState; +import domain.participant.hand.ReadyState; + +public abstract class Participant { + public static final int INITIAL_DRAW_CARDS = 2; + + private final Name name; + HandState hand; + + public Participant(final Name name) { + this(name, new ReadyState()); + } + + public Participant(final Name name, final HandState hand) { + this.name = Objects.requireNonNull(name, "name이 null입니다."); + this.hand = Objects.requireNonNull(hand, "handState가 null입니다."); + } + + public void hitAtFirst(final CardDeck cardDeck) { + for (int count = 0; count < INITIAL_DRAW_CARDS; count++) { + hit(cardDeck); + } + } + + public void hit(final CardDeck cardDeck) { + hand = hand.draw(cardDeck.pick()); + } + + public void stay() { + hand = hand.stay(); + } + + public boolean isFinished() { + return hand.isFinished(); + } + + public boolean isBusted() { + return hand.isBusted(); + } + + public boolean isBlackjack() { + return hand.isBlackjack(); + } + + public int compareScore(final Participant that) { + return Integer.compare(calculateScore(), that.calculateScore()); + } + + private int calculateScore() { + return hand.calculateScore(); + } + + public String getName() { + return name.getName(); + } + + public Hand getHand() { + return hand.getHand(); + } +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java new file mode 100644 index 00000000000..4df8c3f6df7 --- /dev/null +++ b/src/main/java/domain/participant/Player.java @@ -0,0 +1,30 @@ +package domain.participant; + +import java.math.BigDecimal; +import java.util.Objects; + +import domain.BettingMoney; +import domain.Name; +import domain.participant.hand.HandState; + +public class Player extends Participant { + private final BettingMoney bettingMoney; + + public Player(final Name name, final BettingMoney bettingMoney) { + super(name); + this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); + } + + public Player(final Name name, final HandState handState, final BettingMoney bettingMoney) { + super(name, handState); + this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); + } + + public BigDecimal calculateProfit() { + return hand.calculateProfit(bettingMoney.getAmount()); + } + + public BigDecimal getBettingMoney() { + return bettingMoney.getAmount(); + } +} diff --git a/src/main/java/domain/participant/PlayerDecision.java b/src/main/java/domain/participant/PlayerDecision.java new file mode 100644 index 00000000000..8df703f7110 --- /dev/null +++ b/src/main/java/domain/participant/PlayerDecision.java @@ -0,0 +1,34 @@ +package domain.participant; + +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import domain.card.CardDeck; + +public enum PlayerDecision { + HIT("y", (cardDeck, player) -> player.hit(cardDeck)), + STAY("n", (cardDeck, player) -> player.stay()); + + private final String alias; + private final BiConsumer playerAction; + + PlayerDecision(final String alias, final BiConsumer playerAction) { + this.alias = alias; + this.playerAction = playerAction; + } + + public static PlayerDecision of(final String input) { + return Stream.of(values()) + .filter(alias -> alias.isAliasOf(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 입력입니다.")); + } + + public void act(final CardDeck cardDeck, final Player player) { + playerAction.accept(cardDeck, player); + } + + private boolean isAliasOf(final String input) { + return alias.equals(input); + } +} diff --git a/src/main/java/domain/participant/hand/BlackjackState.java b/src/main/java/domain/participant/hand/BlackjackState.java new file mode 100644 index 00000000000..b9c07b66b93 --- /dev/null +++ b/src/main/java/domain/participant/hand/BlackjackState.java @@ -0,0 +1,26 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class BlackjackState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(1.5); + + public BlackjackState(final Hand hand) { + super(hand); + } + + @Override + public boolean isBlackjack() { + return true; + } + + @Override + public boolean isBusted() { + return false; + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } +} diff --git a/src/main/java/domain/participant/hand/BustedState.java b/src/main/java/domain/participant/hand/BustedState.java new file mode 100644 index 00000000000..e18e535ab11 --- /dev/null +++ b/src/main/java/domain/participant/hand/BustedState.java @@ -0,0 +1,26 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class BustedState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(-1); + + public BustedState(final Hand hand) { + super(hand); + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBusted() { + return true; + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } +} diff --git a/src/main/java/domain/participant/hand/FinishedState.java b/src/main/java/domain/participant/hand/FinishedState.java new file mode 100644 index 00000000000..49ec92c3ba8 --- /dev/null +++ b/src/main/java/domain/participant/hand/FinishedState.java @@ -0,0 +1,35 @@ +package domain.participant.hand; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import domain.card.Card; + +public abstract class FinishedState extends StartedState { + public FinishedState(final Hand hand) { + super(hand); + } + + @Override + public BigDecimal calculateProfit(final BigDecimal money) { + return money.multiply(getProfitRate()) + .setScale(0, RoundingMode.HALF_UP); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public HandState draw(final Card card) { + throw new UnsupportedOperationException("hit을 할 수 없는 상태입니다."); + } + + @Override + public HandState stay() { + throw new UnsupportedOperationException("stay를 할 수 없는 상태입니다."); + } + + protected abstract BigDecimal getProfitRate(); +} diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java new file mode 100644 index 00000000000..2b9c64c0a5a --- /dev/null +++ b/src/main/java/domain/participant/hand/Hand.java @@ -0,0 +1,75 @@ +package domain.participant.hand; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import domain.card.Card; + +public class Hand { + private static final int INITIAL_CARD_SIZE = 2; + private static final int MAX_SCORE = 21; + private static final int ACE_UPGRADABLE_SCORE_UPPER_BOUND = 11; + private static final int ACE_UPGRADE_SCORE = 10; + + private final List cards; + + public Hand() { + this.cards = new ArrayList<>(); + } + + public void draw(final Card card) { + cards.add(card); + } + + public boolean isBlackjack() { + return isInitialDraw() && isMaxScore(); + } + + public boolean isMaxScore() { + return calculateScore() == MAX_SCORE; + } + + public boolean isBust() { + return calculateScore() > MAX_SCORE; + } + + public int calculateScore() { + int score = calculateMaxScore(); + return upgradeIfHasAce(score); + } + + public int size() { + return cards.size(); + } + + private int calculateMaxScore() { + return cards.stream() + .mapToInt(card -> card.getFace().getScore()) + .sum(); + } + + private int upgradeIfHasAce(final int score) { + if (hasAce() && isAceUpgradableScore(score)) { + return score + ACE_UPGRADE_SCORE; + } + return score; + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + private boolean isAceUpgradableScore(final int score) { + return score <= ACE_UPGRADABLE_SCORE_UPPER_BOUND; + } + + public boolean isInitialDraw() { + return cards.size() == INITIAL_CARD_SIZE; + } + + public List getCards() { + return Collections.unmodifiableList(cards); + } +} diff --git a/src/main/java/domain/participant/hand/HandState.java b/src/main/java/domain/participant/hand/HandState.java new file mode 100644 index 00000000000..6de492d25cc --- /dev/null +++ b/src/main/java/domain/participant/hand/HandState.java @@ -0,0 +1,25 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +import domain.card.Card; + +public interface HandState { + HandState draw(final Card card); + + HandState stay(); + + boolean isFinished(); + + boolean isBlackjack(); + + boolean isBusted(); + + boolean isOver(final int score); + + int calculateScore(); + + BigDecimal calculateProfit(final BigDecimal money); + + Hand getHand(); +} diff --git a/src/main/java/domain/participant/hand/HitState.java b/src/main/java/domain/participant/hand/HitState.java new file mode 100644 index 00000000000..80653bdf2db --- /dev/null +++ b/src/main/java/domain/participant/hand/HitState.java @@ -0,0 +1,21 @@ +package domain.participant.hand; + +import domain.card.Card; + +public class HitState extends NotFinishedState { + public HitState(final Hand hand) { + super(hand); + } + + @Override + public HandState draw(final Card card) { + hand.draw(card); + if (hand.isMaxScore()) { + return new StayState(hand); + } + if (hand.isBust()) { + return new BustedState(hand); + } + return new HitState(hand); + } +} diff --git a/src/main/java/domain/participant/hand/NotFinishedState.java b/src/main/java/domain/participant/hand/NotFinishedState.java new file mode 100644 index 00000000000..c42b11c87f0 --- /dev/null +++ b/src/main/java/domain/participant/hand/NotFinishedState.java @@ -0,0 +1,34 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public abstract class NotFinishedState extends StartedState { + public NotFinishedState(final Hand hand) { + super(hand); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBusted() { + return false; + } + + @Override + public HandState stay() { + return new StayState(hand); + } + + @Override + public BigDecimal calculateProfit(final BigDecimal money) { + throw new UnsupportedOperationException("수익을 계산할 수 없는 상태입니다."); + } +} diff --git a/src/main/java/domain/participant/hand/ReadyState.java b/src/main/java/domain/participant/hand/ReadyState.java new file mode 100644 index 00000000000..3094fd5db53 --- /dev/null +++ b/src/main/java/domain/participant/hand/ReadyState.java @@ -0,0 +1,21 @@ +package domain.participant.hand; + +import domain.card.Card; + +public class ReadyState extends NotFinishedState { + public ReadyState() { + super(new Hand()); + } + + @Override + public HandState draw(final Card card) { + hand.draw(card); + if (hand.isBlackjack()) { + return new BlackjackState(hand); + } + if (hand.isInitialDraw()) { + return new HitState(hand); + } + return this; + } +} diff --git a/src/main/java/domain/participant/hand/StartedState.java b/src/main/java/domain/participant/hand/StartedState.java new file mode 100644 index 00000000000..b97676ef5f6 --- /dev/null +++ b/src/main/java/domain/participant/hand/StartedState.java @@ -0,0 +1,24 @@ +package domain.participant.hand; + +public abstract class StartedState implements HandState { + protected final Hand hand; + + public StartedState(final Hand hand) { + this.hand = hand; + } + + @Override + public boolean isOver(final int score) { + return calculateScore() > score; + } + + @Override + public int calculateScore() { + return hand.calculateScore(); + } + + @Override + public Hand getHand() { + return hand; + } +} diff --git a/src/main/java/domain/participant/hand/StayState.java b/src/main/java/domain/participant/hand/StayState.java new file mode 100644 index 00000000000..f25896c7154 --- /dev/null +++ b/src/main/java/domain/participant/hand/StayState.java @@ -0,0 +1,26 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class StayState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(1); + + public StayState(final Hand hand) { + super(hand); + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBusted() { + return false; + } +} diff --git a/src/main/java/domain/result/DrawStrategy.java b/src/main/java/domain/result/DrawStrategy.java new file mode 100644 index 00000000000..53c3770eba7 --- /dev/null +++ b/src/main/java/domain/result/DrawStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class DrawStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return dealer.isBlackjack() && player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return false; + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) == 0; + } +} diff --git a/src/main/java/domain/result/GameResult.java b/src/main/java/domain/result/GameResult.java new file mode 100644 index 00000000000..c61c09fbf4c --- /dev/null +++ b/src/main/java/domain/result/GameResult.java @@ -0,0 +1,38 @@ +package domain.result; + +import java.math.BigDecimal; +import java.util.function.Function; +import java.util.stream.Stream; + +import domain.participant.Dealer; +import domain.participant.Player; + +public enum GameResult { + PLAYER_WIN(new PlayerWinStrategy(), Player::calculateProfit), + DRAW(new DrawStrategy(), player -> BigDecimal.ZERO), + PLAYER_LOSE(new PlayerLoseStrategy(), player -> player.getBettingMoney().multiply(BigDecimal.valueOf(-1))); + + private final ResultDecideStrategy resultDecideStrategy; + private final Function profitCalculateFunction; + + GameResult(final ResultDecideStrategy resultDecideStrategy, + final Function profitCalculateFunction) { + this.resultDecideStrategy = resultDecideStrategy; + this.profitCalculateFunction = profitCalculateFunction; + } + + public static GameResult fromDealerAndPlayer(final Dealer dealer, final Player player) { + return Stream.of(values()) + .filter(result -> result.matches(dealer, player)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("결과가 존재하지 않습니다.")); + } + + public BigDecimal calculateProfit(final Player player) { + return profitCalculateFunction.apply(player); + } + + private boolean matches(final Dealer dealer, final Player player) { + return resultDecideStrategy.matches(dealer, player); + } +} \ No newline at end of file diff --git a/src/main/java/domain/result/ParticipantProfit.java b/src/main/java/domain/result/ParticipantProfit.java new file mode 100644 index 00000000000..c3dd3c72448 --- /dev/null +++ b/src/main/java/domain/result/ParticipantProfit.java @@ -0,0 +1,23 @@ +package domain.result; + +import java.math.BigDecimal; + +import domain.participant.Participant; + +public class ParticipantProfit { + private final String name; + private final BigDecimal profit; + + public ParticipantProfit(final Participant participant, final BigDecimal profit) { + this.name = participant.getName(); + this.profit = profit; + } + + public String getName() { + return name; + } + + public BigDecimal getProfit() { + return profit; + } +} diff --git a/src/main/java/domain/result/PlayerLoseStrategy.java b/src/main/java/domain/result/PlayerLoseStrategy.java new file mode 100644 index 00000000000..373b40c82d6 --- /dev/null +++ b/src/main/java/domain/result/PlayerLoseStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class PlayerLoseStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return dealer.isBlackjack() && !player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return player.isBusted(); + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) > 0; + } +} diff --git a/src/main/java/domain/result/PlayerWinStrategy.java b/src/main/java/domain/result/PlayerWinStrategy.java new file mode 100644 index 00000000000..53240753ff7 --- /dev/null +++ b/src/main/java/domain/result/PlayerWinStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class PlayerWinStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return !dealer.isBlackjack() && player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return dealer.isBusted() && !player.isBusted(); + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) < 0; + } +} diff --git a/src/main/java/domain/result/ResultDecideStrategy.java b/src/main/java/domain/result/ResultDecideStrategy.java new file mode 100644 index 00000000000..328af3f65ae --- /dev/null +++ b/src/main/java/domain/result/ResultDecideStrategy.java @@ -0,0 +1,22 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public interface ResultDecideStrategy { + default boolean matches(final Dealer dealer, final Player player) { + if (dealer.isBlackjack() || player.isBlackjack()) { + return matchByBlackjack(dealer, player); + } + if (dealer.isBusted() || player.isBusted()) { + return matchByBust(dealer, player); + } + return matchByScore(dealer, player); + } + + boolean matchByBlackjack(final Dealer dealer, final Player player); + + boolean matchByBust(final Dealer dealer, final Player player); + + boolean matchByScore(final Dealer dealer, final Player player); +} diff --git a/src/main/java/empty.txt b/src/main/java/empty.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/java/service/DrawService.java b/src/main/java/service/DrawService.java new file mode 100644 index 00000000000..71d001fe570 --- /dev/null +++ b/src/main/java/service/DrawService.java @@ -0,0 +1,26 @@ +package service; + +import java.util.List; + +import domain.card.CardDeck; +import domain.participant.Dealer; +import domain.participant.Player; +import domain.participant.PlayerDecision; + +public class DrawService { + public void drawInitialCards(final CardDeck cardDeck, final Dealer dealer, final List players) { + dealer.hitAtFirst(cardDeck); + for (final Player player : players) { + player.hitAtFirst(cardDeck); + } + } + + public void drawDealer(final CardDeck cardDeck, final Dealer dealer) { + dealer.hit(cardDeck); + } + + public void drawPlayer(final CardDeck cardDeck, final Player player, final String decision) { + PlayerDecision playerDecision = PlayerDecision.of(decision); + playerDecision.act(cardDeck, player); + } +} diff --git a/src/main/java/service/PlayerService.java b/src/main/java/service/PlayerService.java new file mode 100644 index 00000000000..d3d5246a138 --- /dev/null +++ b/src/main/java/service/PlayerService.java @@ -0,0 +1,39 @@ +package service; + +import java.util.ArrayList; +import java.util.List; + +import domain.BettingMoney; +import domain.Name; +import domain.participant.Player; + +public class PlayerService { + private static final int MIN_PLAYERS = 2; + private static final int MAX_PLAYERS = 8; + + public List createPlayers(List names, List bettingMonies) { + validateSize(names, bettingMonies); + validateLength(names); + List players = new ArrayList<>(); + for (int count = 0; count < names.size(); count++) { + players.add(new Player(names.get(count), bettingMonies.get(count))); + } + return players; + } + + private void validateSize(final List names, final List bettingMonies) { + if (names.size() != bettingMonies.size()) { + throw new IllegalArgumentException("names와 bettingMonies의 크기가 일치하지 않습니다.\n" + + "names size: " + names.size() + "\n" + + "bettingMonies size: " + bettingMonies.size()); + } + } + + private void validateLength(final List names) { + int totalPlayer = names.size(); + if (totalPlayer < MIN_PLAYERS || totalPlayer > MAX_PLAYERS) { + throw new IllegalArgumentException("참여자의 수가 올바르지 않습니다.\n" + + "players size: " + names.size()); + } + } +} diff --git a/src/main/java/service/ProfitService.java b/src/main/java/service/ProfitService.java new file mode 100644 index 00000000000..3067f09c63f --- /dev/null +++ b/src/main/java/service/ProfitService.java @@ -0,0 +1,30 @@ +package service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import domain.participant.Dealer; +import domain.participant.Player; +import domain.result.GameResult; +import domain.result.ParticipantProfit; + +public class ProfitService { + public List calculateProfits(final Dealer dealer, final List players) { + List participantProfits = new ArrayList<>(); + for (final Player player : players) { + GameResult gameResult = GameResult.fromDealerAndPlayer(dealer, player); + BigDecimal profit = gameResult.calculateProfit(player); + participantProfits.add(new ParticipantProfit(player, profit)); + } + ParticipantProfit dealerProfit = new ParticipantProfit(dealer, calculateDealerProfit(participantProfits)); + participantProfits.add(0, dealerProfit); + return participantProfits; + } + + private BigDecimal calculateDealerProfit(final List profits) { + return profits.stream() + .map(ParticipantProfit::getProfit) + .reduce(BigDecimal.ZERO, BigDecimal::subtract); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000000..fcb6f2dc15f --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,32 @@ +package view; + +import java.util.Scanner; + +import domain.Name; +import domain.participant.Player; + +public class InputView { + private final Scanner scanner; + + public InputView(final Scanner scanner) { + this.scanner = scanner; + } + + public String inputNames() { + System.out.println(); + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + return scanner.nextLine(); + } + + public int inputBettingMoney(final Name name) { + System.out.println(); + System.out.printf("%s의 배팅 금액은?\n", name.getName()); + return Integer.parseInt(scanner.nextLine()); + } + + public String inputPlayerDecision(final Player player) { + System.out.println(); + System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)\n", player.getName()); + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..c4d589653da --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,72 @@ +package view; + +import java.io.PrintStream; +import java.util.List; +import java.util.stream.Collectors; + +import domain.card.Card; +import domain.participant.Dealer; +import domain.participant.Participant; +import domain.participant.Player; +import domain.participant.hand.Hand; +import domain.result.ParticipantProfit; + +public class OutputView { + + private static final String DELIMITER = ", "; + + public void printHandsAtFirst(final Dealer dealer, final List players) { + System.out.println(); + System.out.printf("%s와 %s에게 2장의 나누었습니다.\n", dealer.getName(), renderNames(players)); + + System.out.printf("딜러카드: %s\n", dealer.getHand().getCards().get(1).alias()); + players.forEach(this::printHand); + } + + public void printHand(final Participant participant) { + System.out.println(renderHand(participant)); + } + + public void printDealerHitMessage() { + System.out.println(); + System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다."); + } + + public void printHandsAtLast(final Dealer dealer, final List players) { + System.out.println(); + printHandAtLast(dealer); + players.forEach(this::printHandAtLast); + } + + public void printProfits(final List participantProfits) { + System.out.println(); + System.out.println("## 최종 수익"); + participantProfits.forEach(this::printProfit); + } + + private void printHandAtLast(final Participant participant) { + System.out.printf("%s - 결과: %d\n", renderHand(participant), participant.getHand().calculateScore()); + } + + private String renderHand(final Participant participant) { + return String.format("%s카드: %s", participant.getName(), renderCards(participant.getHand())); + } + + private String renderNames(final List players) { + return players.stream() + .map(Participant::getName) + .collect(Collectors.joining(DELIMITER)); + } + + private String renderCards(final Hand hand) { + return hand.getCards() + .stream() + .map(Card::alias) + .collect(Collectors.joining(DELIMITER)); + } + + private PrintStream printProfit(final ParticipantProfit participantProfit) { + return System.out.printf("%s: %d\n", participantProfit.getName(), + participantProfit.getProfit().longValueExact()); + } +} diff --git a/src/test/java/domain/BettingMoneyTest.java b/src/test/java/domain/BettingMoneyTest.java new file mode 100644 index 00000000000..945c355f142 --- /dev/null +++ b/src/test/java/domain/BettingMoneyTest.java @@ -0,0 +1,44 @@ +package domain; + +import static java.math.BigDecimal.valueOf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BettingMoneyTest { + @DisplayName("constructor: 금액을 입력받아 인스턴스 생성") + @ValueSource(ints = {100, 1_000, 5_000, 10_500}) + @ParameterizedTest + void constructor(final int amount) { + assertThat(new BettingMoney(amount)).isInstanceOf(BettingMoney.class); + } + + @DisplayName("constructor: 최소 베팅 금액을 충족하지 못한 경우 예외 발생") + @ValueSource(ints = {0, 99}) + @ParameterizedTest + void constructor_LackOfAmount_ExceptionThrown(final int amount) { + assertThatThrownBy(() -> new BettingMoney(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("베팅 최소 금액을 충족하지 못했습니다"); + } + + @DisplayName("constructor: 금액의 단위가 못한 경우 예외 발생") + @Test + void constructor_BettingUnitMismatch_ExceptionThrown() { + assertThatThrownBy(() -> new BettingMoney(101)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("금액의 단위가 올바르지 않습니다"); + } + + @DisplayName("getAmount: 금액을 반환") + @Test + void getAmount() { + BettingMoney bettingMoney = new BettingMoney(1_000); + + assertThat(bettingMoney.getAmount()).isEqualTo(valueOf(1_000)); + } +} diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java new file mode 100644 index 00000000000..397751739ba --- /dev/null +++ b/src/test/java/domain/Fixture.java @@ -0,0 +1,71 @@ +package domain; + +import static domain.card.Face.ACE; +import static domain.card.Face.FIVE; +import static domain.card.Face.FOUR; +import static domain.card.Face.JACK; +import static domain.card.Face.SEVEN; +import static domain.card.Face.SIX; +import static domain.card.Face.THREE; +import static domain.card.Face.TWO; +import static domain.card.Suit.CLUB; +import static domain.card.Suit.SPADE; + +import java.util.Arrays; +import java.util.List; + +import domain.card.Card; +import domain.participant.hand.Hand; +import domain.participant.hand.HandState; +import domain.participant.hand.ReadyState; + +public class Fixture { + public static final List CARDS = Card.values(); + + public static final Name SOUTHJUN = new Name("southjun"); + public static final Name NORTHJUN = new Name("northjun"); + public static final Name WESTJUN = new Name("westjun"); + public static final Name EASTJUN = new Name("eastjun"); + + public static final BettingMoney HUNDRED_BETTING_MONEY = new BettingMoney(1_000_000); + + public static final Card ACE_SCORE = Card.fromFaceAndSuit(ACE, CLUB); + public static final Card TWO_SCORE = Card.fromFaceAndSuit(TWO, CLUB); + public static final Card THREE_SCORE = Card.fromFaceAndSuit(THREE, CLUB); + public static final Card FOUR_SCORE = Card.fromFaceAndSuit(FOUR, CLUB); + public static final Card FIVE_SCORE = Card.fromFaceAndSuit(FIVE, CLUB); + public static final Card SIX_SCORE = Card.fromFaceAndSuit(SIX, CLUB); + public static final Card SEVEN_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); + public static final Card TEN_SCORE = Card.fromFaceAndSuit(JACK, SPADE); + + public static final List BUSTED_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, TWO_SCORE); + public static final List BUSTED_BY_ACE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE, ACE_SCORE); + public static final List BLACKJACK_CARDS = Arrays.asList(TEN_SCORE, ACE_SCORE); + public static final List MAX_SCORE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE); + public static final List DEALER_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TWO_SCORE, TWO_SCORE); + public static final List DEALER_HITTABLE_UPPER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SIX_SCORE); + public static final List DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SEVEN_SCORE); + + public static final Hand HITTABLE_LOW_HAND = createHand(DEALER_HITTABLE_LOWER_BOUND_CARDS); + public static final Hand HITTABLE_HIGH_HAND = createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS); + public static final Hand BLACKJACK_HAND = createHand(BLACKJACK_CARDS); + public static final Hand BUSTED_HAND = createHand(BUSTED_CARDS); + + public static final HandState HITTABLE_HAND_STATE = createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS); + + public static Hand createHand(List cards) { + Hand hand = new Hand(); + for (final Card card : cards) { + hand.draw(card); + } + return hand; + } + + public static HandState createHandState(List cards) { + HandState handState = new ReadyState(); + for (final Card card : cards) { + handState = handState.draw(card); + } + return handState; + } +} diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java new file mode 100644 index 00000000000..b9ce58d4a4a --- /dev/null +++ b/src/test/java/domain/NameTest.java @@ -0,0 +1,50 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + @DisplayName("constructor: 1~5자 사이의 이름을 입력받아 인스턴스 생성") + @ValueSource(strings = {"뭐", "hello", " hello ", " 오더하기오는십이예요", " hello"}) + @ParameterizedTest + void constructor(final String name) { + assertThat(new Name(name)).isInstanceOf(Name.class); + } + + @DisplayName("constructor: 입력받은 이름이 null이면 예외 발생") + @Test + void constructor_NameIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Name(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("이름이 null입니다"); + } + + @DisplayName("constructor: 길이가 올바르지 않은 이름을 입력받아 예외 발생") + @ValueSource(strings = {"", " ", " ", "열한자는허락할수없어요"}) + @ParameterizedTest + void constructor_InvalidNameLength_ExceptionThrown(final String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름의 길이가 올바르지 않습니다"); + } + + @DisplayName("fromComma: 콤마 단위로 이름을 입력받아 이름 리스트 생성") + @Test + void fromComma() { + assertThat(Name.fromComma("a, hell, hello, 메롱, the, he")).hasSize(6); + } + + @DisplayName("getName: 이름을 반환") + @ValueSource(strings = {" hell", "hell ", " hell "}) + @ParameterizedTest + void getName(final String nameWithSpace) { + Name name = new Name(nameWithSpace); + + assertThat(name.getName()).isEqualTo("hell"); + } +} diff --git a/src/test/java/domain/card/CardDeckFactoryTest.java b/src/test/java/domain/card/CardDeckFactoryTest.java new file mode 100644 index 00000000000..e1dbecdbfcb --- /dev/null +++ b/src/test/java/domain/card/CardDeckFactoryTest.java @@ -0,0 +1,13 @@ +package domain.card; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardDeckFactoryTest { + @DisplayName("constructor: 랜덤 카드 덱 생성") + @Test + void createRandomCardDeck() { + Assertions.assertThat(CardDeckFactory.createRandomCardDeck()).isInstanceOf(RandomCardDeck.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java new file mode 100644 index 00000000000..8503dc6a931 --- /dev/null +++ b/src/test/java/domain/card/CardTest.java @@ -0,0 +1,77 @@ +package domain.card; + +import static domain.card.Face.ACE; +import static domain.card.Suit.CLUB; +import static domain.card.Suit.HEART; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class CardTest { + @DisplayName("fromFaceAndSuit: 카드 한 장을 생성") + @Test + void fromFaceAndSuit() { + assertThat(Card.fromFaceAndSuit(ACE, CLUB)).isInstanceOf(Card.class); + } + + @DisplayName("fromFaceAndSuit: face 또는 suit가 null인 경우 예외 발생") + @CsvSource(value = {", CLUB", "TEN,"}) + @ParameterizedTest + void fromFaceAndSuit_FaceOrSuitIsNull_ExceptionThrown(final Face face, final Suit suit) { + assertThatThrownBy(() -> Card.fromFaceAndSuit(face, suit)) + .isInstanceOf(CardNotFoundException.class) + .hasMessageContaining("카드가 존재하지 않습니다"); + } + + @DisplayName("values: 전체 카드를 반환") + @Test + void values() { + assertThat(Card.values()).hasSize(52); + } + + @DisplayName("isAce: 카드가 Ace인지 판별") + @CsvSource(value = {"ACE, true", "TEN, false"}) + @ParameterizedTest + void isAce(final Face face, final boolean expect) { + Card card = Card.fromFaceAndSuit(face, CLUB); + + assertThat(card.isAce()).isEqualTo(expect); + } + + @DisplayName("isCardOf: 카드의 face와 suit가 일치하는지 확인") + @CsvSource(value = {"ACE, CLUB, true", "TEN, CLUB, false", "ACE, DIAMOND, false"}) + @ParameterizedTest + void isAce(final Face face, final Suit suit, final boolean expect) { + Card card = Card.fromFaceAndSuit(ACE, CLUB); + + assertThat(card.isCardOf(face, suit)).isEqualTo(expect); + } + + @DisplayName("getFace: face를 반환") + @Test + void getFace() { + Card card = Card.fromFaceAndSuit(ACE, CLUB); + + assertThat(card.getFace()).isEqualTo(ACE); + } + + @DisplayName("getSuit: suit를 반환") + @Test + void getSuit() { + Card card = Card.fromFaceAndSuit(ACE, CLUB); + + assertThat(card.getSuit()).isEqualTo(CLUB); + } + + @DisplayName("alias: 출력용 텍스트 반환") + @Test + void alias() { + Card card = Card.fromFaceAndSuit(ACE, HEART); + + assertThat(card.alias()).isEqualTo("❤️A"); + } +} diff --git a/src/test/java/domain/card/FakeCardDeck.java b/src/test/java/domain/card/FakeCardDeck.java new file mode 100644 index 00000000000..0fb7b97b869 --- /dev/null +++ b/src/test/java/domain/card/FakeCardDeck.java @@ -0,0 +1,22 @@ +package domain.card; + +import java.util.List; +import java.util.Stack; + +public class FakeCardDeck implements CardDeck { + private final Stack cards; + + public FakeCardDeck(final List cards) { + Stack stackedCards = new Stack<>(); + stackedCards.addAll(cards); + this.cards = stackedCards; + } + + @Override + public Card pick() { + if (cards.isEmpty()) { + throw new EmptyCardDeckException("카드를 모두 소모하여 더 뽑을 수 없습니다."); + } + return cards.pop(); + } +} diff --git a/src/test/java/domain/card/RandomCardDeckTest.java b/src/test/java/domain/card/RandomCardDeckTest.java new file mode 100644 index 00000000000..0770be39eca --- /dev/null +++ b/src/test/java/domain/card/RandomCardDeckTest.java @@ -0,0 +1,61 @@ +package domain.card; + +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.CARDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RandomCardDeckTest { + private RandomCardDeck cardDeck; + + @BeforeEach + void setUp() { + cardDeck = new RandomCardDeck(CARDS); + } + + @DisplayName("constructor: 카드 덱 생성") + @Test + void constructor() { + assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); + } + + @DisplayName("constructor: 카드 리스트가 null인 경우 예외 발생") + @Test + void constructor_CardsIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new RandomCardDeck(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("카드 리스트가 null입니다"); + } + + @DisplayName("constructor: 카드 리스트의 크기가 0인 경우 예외 발생") + @Test + void constructor_CardsIsEmpty_ExceptionThrown() { + assertThatThrownBy(() -> new RandomCardDeck(new ArrayList<>())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("카드 리스트의 크기가 0입니다"); + } + + @DisplayName("pick: 카드 한 장을 뽑음") + @Test + void pick() { + assertThat(cardDeck.pick()).isInstanceOf(Card.class); + } + + @DisplayName("pick: 카드 한 장을 뽑을 때 더 이상 카드가 없으면 예외 발생") + @Test + void pick_DeckIsEmpty_ExceptionThrown() { + cardDeck = new RandomCardDeck(Collections.singletonList(ACE_SCORE)); + cardDeck.pick(); + + assertThatThrownBy(() -> cardDeck.pick()) + .isInstanceOf(EmptyCardDeckException.class) + .hasMessageContaining("카드를 모두 소모하여 더 뽑을 수 없습니다"); + } +} diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java new file mode 100644 index 00000000000..31c7d6899f5 --- /dev/null +++ b/src/test/java/domain/participant/DealerTest.java @@ -0,0 +1,83 @@ +package domain.participant; + +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.DEALER_HITTABLE_LOWER_BOUND_CARDS; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHandState; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.card.CardDeck; +import domain.card.FakeCardDeck; + +class DealerTest { + @DisplayName("constructor: 딜러 인스턴스 생성") + @Test + void constructor_NoArguments() { + assertThat(new Dealer()).isInstanceOf(Dealer.class); + } + + @DisplayName("constructor: 딜러 인스턴스 생성") + @Test + void constructor_OneArguments() { + assertThat(new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS))).isInstanceOf(Dealer.class); + } + + @DisplayName("hitAtFirst: 최초로 카드를 두 장 뽑음") + @Test + void hitAtFirst() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Arrays.asList(ACE_SCORE, TWO_SCORE)); + + dealer.hitAtFirst(cardDeck); + + assertAll( + () -> assertThat(dealer.isBusted()).isFalse() + ); + } + + @DisplayName("hit: 카드 한 장을 받고 16 이하이면 Hittable 상태 유지") + @Test + void hit_HittableState() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(ACE_SCORE)); + + dealer.hit(cardDeck); + + assertAll( + () -> assertThat(dealer.isFinished()).isFalse(), + () -> assertThat(dealer.isBlackjack()).isFalse(), + () -> assertThat(dealer.isBusted()).isFalse() + ); + } + + @DisplayName("hit: 카드 한 장을 받고 16 초과하면 stay 상태로 전이") + @Test + void hit_ChangeToStayState() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(ACE_SCORE)); + + dealer.hit(cardDeck); + + assertAll( + () -> assertThat(dealer.isFinished()).isTrue(), + () -> assertThat(dealer.isBlackjack()).isFalse(), + () -> assertThat(dealer.isBusted()).isFalse() + ); + } + + @DisplayName("getHand: hand를 반환") + @Test + void getHand() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + + assertThat(dealer.getHand()).isNotNull(); + } +} diff --git a/src/test/java/domain/participant/PlayerDecisionTest.java b/src/test/java/domain/participant/PlayerDecisionTest.java new file mode 100644 index 00000000000..973ad359f89 --- /dev/null +++ b/src/test/java/domain/participant/PlayerDecisionTest.java @@ -0,0 +1,41 @@ +package domain.participant; + +import static domain.Fixture.EASTJUN; +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.TWO_SCORE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Collections; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import domain.card.CardDeck; +import domain.card.FakeCardDeck; + +class PlayerDecisionTest { + @DisplayName("of: PlayerDecision 반환") + @CsvSource(value = {"y, HIT", "n, STAY"}) + @ParameterizedTest + void of_ValidInput_ReturnPlayerDecision(final String input, final PlayerDecision expect) { + assertThat(PlayerDecision.of(input)).isEqualTo(expect); + } + + @DisplayName("of: PlayerDecision 반환") + @CsvSource(value = {"HIT, false", "STAY, true"}) + @ParameterizedTest + void act(final PlayerDecision playerDecision, final boolean isFinished) { + Player player = new Player(EASTJUN, HUNDRED_BETTING_MONEY); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TWO_SCORE)); + + playerDecision.act(cardDeck, player); + + assertAll( + () -> assertThat(player.isFinished()).isEqualTo(isFinished), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() + ); + } +} \ No newline at end of file diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java new file mode 100644 index 00000000000..166e29ca58c --- /dev/null +++ b/src/test/java/domain/participant/PlayerTest.java @@ -0,0 +1,117 @@ +package domain.participant; + +import static domain.Fixture.EASTJUN; +import static domain.Fixture.HITTABLE_HAND_STATE; +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.TEN_SCORE; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.WESTJUN; +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 java.math.BigDecimal; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.card.CardDeck; +import domain.card.FakeCardDeck; + +class PlayerTest { + private Player player; + + @BeforeEach + void setUp() { + player = new Player(EASTJUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY); + } + + @DisplayName("constructor: 사용자 생성") + @Test + void constructor_TwoArguments() { + assertThat(new Player(EASTJUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + } + + @DisplayName("constructor: 사용자 생성") + @Test + void constructor_ThreeArguments() { + assertThat(new Player(EASTJUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + } + + @DisplayName("constructor: Name이 null이면 예외 발생") + @Test + void constructor_NameIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(null, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("name이 null입니다"); + } + + @DisplayName("constructor: HandState가 null이면 예외 발생") + @Test + void constructor_HandStateIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(WESTJUN, null, HUNDRED_BETTING_MONEY)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("handState가 null입니다"); + } + + @DisplayName("constructor: BettingMoney가 null이면 예외 발생") + @Test + void constructor_BettingMoneyIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(EASTJUN, HITTABLE_HAND_STATE, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("bettingMoney가 null입니다"); + } + + @DisplayName("hit: 카드를 한 장 뽑고 상태 전이") + @Test + void hit() { + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TWO_SCORE)); + player.hit(cardDeck); + + assertAll( + () -> assertThat(player.isFinished()).isFalse(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() + ); + } + + @DisplayName("hit: 카드를 한 장 뽑고 버스트 상태로 전이") + @Test + void hit_Busted() { + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TEN_SCORE)); + player.hit(cardDeck); + + assertAll( + () -> assertThat(player.isFinished()).isTrue(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isTrue() + ); + } + + @DisplayName("stay: 카드를 한 장 뽑고 상태 전이") + @Test + void stay() { + player.stay(); + + assertAll( + () -> assertThat(player.isFinished()).isTrue(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() + ); + } + + @DisplayName("calculateProfit: 수익률 계산") + @Test + void calculateProfit() { + player.stay(); + assertThat(player.calculateProfit()).isEqualTo(BigDecimal.valueOf(1_000_000)); + } + + @DisplayName("getName: 이름을 반환") + @Test + void getName() { + assertThat(player.getName()).isEqualTo("eastjun"); + } +} diff --git a/src/test/java/domain/participant/hand/BlackjackStateTest.java b/src/test/java/domain/participant/hand/BlackjackStateTest.java new file mode 100644 index 00000000000..4e9fe90118f --- /dev/null +++ b/src/test/java/domain/participant/hand/BlackjackStateTest.java @@ -0,0 +1,54 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BlackjackStateTest { + private static final BlackjackState BLACKJACK_STATE = new BlackjackState(createHand(BLACKJACK_CARDS)); + + @DisplayName("constructor: blackjack 상태 생성") + @Test + void constructor() { + assertThat(new BlackjackState(createHand(BLACKJACK_CARDS))) + .isInstanceOf(BlackjackState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> BLACKJACK_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isBusted: 버스트가 아님") + @Test + void isBusted() { + assertThat(BLACKJACK_STATE.isBusted()).isFalse(); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(BLACKJACK_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(BLACKJACK_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateProfit() { + assertThat(BLACKJACK_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(1_500)); + } +} diff --git a/src/test/java/domain/participant/hand/BustedStateTest.java b/src/test/java/domain/participant/hand/BustedStateTest.java new file mode 100644 index 00000000000..8f96afae587 --- /dev/null +++ b/src/test/java/domain/participant/hand/BustedStateTest.java @@ -0,0 +1,49 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.BUSTED_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BustedStateTest { + private static final BustedState BUSTED_STATE = new BustedState(createHand(BUSTED_CARDS)); + + @DisplayName("constructor: busted 상태 생성") + @Test + void constructor() { + assertThat(new BustedState(createHand(BLACKJACK_CARDS))) + .isInstanceOf(BustedState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> BUSTED_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(BUSTED_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(BUSTED_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateProfit() { + assertThat(BUSTED_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(-1_000)); + } +} diff --git a/src/test/java/domain/participant/hand/HandTest.java b/src/test/java/domain/participant/hand/HandTest.java new file mode 100644 index 00000000000..3857697157b --- /dev/null +++ b/src/test/java/domain/participant/hand/HandTest.java @@ -0,0 +1,142 @@ +package domain.participant.hand; + +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.BUSTED_BY_ACE_CARDS; +import static domain.Fixture.BUSTED_CARDS; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS; +import static domain.Fixture.MAX_SCORE_CARDS; +import static domain.Fixture.THREE_SCORE; +import static domain.Fixture.TWO_SCORE; +import static domain.card.Face.ACE; +import static domain.card.Suit.SPADE; +import static org.assertj.core.api.Assertions.assertThat; + +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; + +import domain.card.Card; + +class HandTest { + private static Stream createCardsAndScore() { + return Stream.of( + Arguments.of(BUSTED_CARDS, 22), + Arguments.of(BUSTED_BY_ACE_CARDS, 22), + Arguments.of(BLACKJACK_CARDS, 21), + Arguments.of(MAX_SCORE_CARDS, 21), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, 16), + Arguments.of(DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS, 17) + ); + } + + private static Stream createCardsAndIsBlackjack() { + return Stream.of( + Arguments.of(BUSTED_BY_ACE_CARDS, false), + Arguments.of(BLACKJACK_CARDS, true), + Arguments.of(MAX_SCORE_CARDS, false), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, false) + ); + } + + private static Stream createCardsAndIsBust() { + return Stream.of( + Arguments.of(BUSTED_BY_ACE_CARDS, true), + Arguments.of(BLACKJACK_CARDS, false), + Arguments.of(MAX_SCORE_CARDS, false), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, false) + ); + } + + @DisplayName("constructor: Hand 인스턴스 생성") + @Test + void constructor() { + assertThat(new Hand()).isInstanceOf(Hand.class); + } + + @DisplayName("draw: 카드를 한 장 추가") + @Test + void draw() { + Hand hand = new Hand(); + Card card = Card.fromFaceAndSuit(ACE, SPADE); + + hand.draw(card); + assertThat(hand.size()).isEqualTo(1); + } + + @DisplayName("isBlackjack: 카드가 블랙잭인지 여부를 판단") + @MethodSource("createCardsAndIsBlackjack") + @ParameterizedTest + void isBlackjack(final List cards, final boolean expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.isBlackjack()).isEqualTo(expect); + } + + @DisplayName("isBust: 카드가 버스트인지 여부를 판단") + @MethodSource("createCardsAndIsBust") + @ParameterizedTest + void isBust(final List cards, final boolean expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.isBust()).isEqualTo(expect); + } + + @DisplayName("calculateScore: 카드의 점수를 계산") + @MethodSource("createCardsAndScore") + @ParameterizedTest + void calculateScore(final List cards, final int expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.calculateScore()).isEqualTo(expect); + } + + @DisplayName("isInitialDraw: 두 장의 카드를 가지고 있으면 true 반환") + @Test + void isInitialDraw_HasTwoCards_ReturnTrue() { + Hand hand = new Hand(); + + hand.draw(ACE_SCORE); + hand.draw(TWO_SCORE); + + assertThat(hand.isInitialDraw()).isTrue(); + } + + @DisplayName("isInitialDraw: 두 장의 카드를 가지고 있으면 true 반환") + @Test + void isInitialDraw_NotHasTwoCards_ReturnFalse() { + Hand hand = new Hand(); + + hand.draw(ACE_SCORE); + hand.draw(TWO_SCORE); + hand.draw(THREE_SCORE); + + assertThat(hand.isInitialDraw()).isFalse(); + } + + @DisplayName("getCards: 카드 리스트를 반환") + @Test + void getCards() { + Hand hand = new Hand(); + + assertThat(hand.getCards()).isNotNull(); + } +} diff --git a/src/test/java/domain/participant/hand/HitStateTest.java b/src/test/java/domain/participant/hand/HitStateTest.java new file mode 100644 index 00000000000..57096793be0 --- /dev/null +++ b/src/test/java/domain/participant/hand/HitStateTest.java @@ -0,0 +1,77 @@ +package domain.participant.hand; + +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.FIVE_SCORE; +import static domain.Fixture.FOUR_SCORE; +import static domain.Fixture.SIX_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +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; + +import domain.card.Card; + +class HitStateTest { + private HitState hitState; + + private static Stream createCardAndIsNextBust() { + return Stream.of( + Arguments.of(FOUR_SCORE, HitState.class), + Arguments.of(FIVE_SCORE, StayState.class), + Arguments.of(SIX_SCORE, BustedState.class) + ); + } + + @BeforeEach + void setUp() { + hitState = new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + } + + @DisplayName("constructor: 초기 카드로 HitState 생성") + @Test + void constructor() { + assertThat(new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS))) + .isInstanceOf(HitState.class); + } + + @DisplayName("draw: 카드를 한 장 받아 다음 상태로 전이") + @MethodSource("createCardAndIsNextBust") + @ParameterizedTest + void draw(final Card card, final Class expect) { + assertThat(hitState.draw(card)).isInstanceOf(expect); + } + + @DisplayName("isFinished: 끝나지 않았음을 반환") + @Test + void isFinished() { + assertThat(hitState.isFinished()).isFalse(); + } + + @DisplayName("stay: 턴을 마치고 상태 전이") + @Test + void stay() { + assertThat(hitState.stay()).isInstanceOf(StayState.class); + } + + @DisplayName("calculateHand: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void calculateHand() { + assertThatThrownBy(() -> hitState.calculateProfit(BigDecimal.valueOf(1_000))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("getHand: 가지고 있는 패를 반환") + @Test + void getHand() { + assertThat(hitState.getHand()).isInstanceOf(Hand.class); + } +} diff --git a/src/test/java/domain/participant/hand/ReadyStateTest.java b/src/test/java/domain/participant/hand/ReadyStateTest.java new file mode 100644 index 00000000000..3269360a83f --- /dev/null +++ b/src/test/java/domain/participant/hand/ReadyStateTest.java @@ -0,0 +1,46 @@ +package domain.participant.hand; + +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.TEN_SCORE; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReadyStateTest { + private ReadyState readyState; + + @BeforeEach + void setUp() { + readyState = new ReadyState(); + } + + @DisplayName("constructor: Ready 상태 생성") + @Test + void constructor() { + assertThat(new ReadyState()).isInstanceOf(ReadyState.class); + } + + @DisplayName("draw: 두 장 드로우하여 Blackjack 상태로 전이") + @Test + void draw() { + HandState handState = new ReadyState(); + handState = handState.draw(ACE_SCORE); + handState = handState.draw(TEN_SCORE); + + assertThat(handState.isBlackjack()).isTrue(); + } + + @DisplayName("isBlackjack: 블랙잭이 아님을 확인") + @Test + void isBlackjack() { + assertThat(readyState.isBlackjack()).isFalse(); + } + + @DisplayName("isBusted: 버스트가 아님을 확인") + @Test + void isBusted() { + assertThat(readyState.isBusted()).isFalse(); + } +} diff --git a/src/test/java/domain/participant/hand/StayStateTest.java b/src/test/java/domain/participant/hand/StayStateTest.java new file mode 100644 index 00000000000..ebc81dd149b --- /dev/null +++ b/src/test/java/domain/participant/hand/StayStateTest.java @@ -0,0 +1,48 @@ +package domain.participant.hand; + +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StayStateTest { + private static final StayState STAY_STATE = new StayState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + + @DisplayName("constructor: stay 상태 생성") + @Test + void constructor() { + assertThat(new StayState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS))) + .isInstanceOf(StayState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> STAY_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(STAY_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(STAY_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateHand() { + assertThat(STAY_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(1_000)); + } +} diff --git a/src/test/java/domain/result/GameResultTest.java b/src/test/java/domain/result/GameResultTest.java new file mode 100644 index 00000000000..b631a175b3a --- /dev/null +++ b/src/test/java/domain/result/GameResultTest.java @@ -0,0 +1,117 @@ +package domain.result; + +import static domain.Fixture.BLACKJACK_HAND; +import static domain.Fixture.BUSTED_HAND; +import static domain.Fixture.EASTJUN; +import static domain.Fixture.HITTABLE_HIGH_HAND; +import static domain.Fixture.HITTABLE_LOW_HAND; +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.NORTHJUN; +import static domain.Fixture.SOUTHJUN; +import static domain.Fixture.WESTJUN; +import static domain.result.GameResult.DRAW; +import static domain.result.GameResult.PLAYER_LOSE; +import static domain.result.GameResult.PLAYER_WIN; +import static domain.result.GameResult.fromDealerAndPlayer; +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.participant.Dealer; +import domain.participant.Player; +import domain.participant.hand.BlackjackState; +import domain.participant.hand.BustedState; +import domain.participant.hand.StayState; + +public class GameResultTest { + private static final Dealer BLACKJACK_DEALER = new Dealer(new BlackjackState(BLACKJACK_HAND)); + private static final Dealer HIGH_SCORE_DEALER = new Dealer(new StayState(HITTABLE_HIGH_HAND)); + private static final Dealer LOW_SCORE_DEALER = new Dealer(new StayState(HITTABLE_LOW_HAND)); + private static final Dealer BUSTED_DEALER = new Dealer(new BustedState(BUSTED_HAND)); + + private static final Player BLACKJACK_PLAYER = + new Player(WESTJUN, new BlackjackState(BLACKJACK_HAND), HUNDRED_BETTING_MONEY); + private static final Player HIGH_SCORE_PLAYER = + new Player(EASTJUN, new StayState(HITTABLE_HIGH_HAND), HUNDRED_BETTING_MONEY); + private static final Player LOW_SCORE_PLAYER = + new Player(SOUTHJUN, new StayState(HITTABLE_LOW_HAND), HUNDRED_BETTING_MONEY); + private static final Player BUSTED_PLAYER = + new Player(NORTHJUN, new BustedState(BUSTED_HAND), HUNDRED_BETTING_MONEY); + + @DisplayName("fromDealerAndPlayer: 플레이어가 블랙잭 승리") + @Test + void fromDealerAndPlayer_PlayerBlackjackWin() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, BLACKJACK_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("fromDealerAndPlayer: 블랙잭 무승부") + @Test + void fromDealerAndPlayer_BlackjackDraw() { + assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, BLACKJACK_PLAYER)).isEqualTo(DRAW); + } + + @DisplayName("fromDealerAndPlayer: 딜러가 블랙잭 승리") + @Test + void fromDealerAndPlayer_DealerBlackjackWin() { + assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); + } + + @DisplayName("fromDealerAndPlayer: 플레이어가 버스트 패배") + @Test + void fromDealerAndPlayer_PlayerBustLose() { + assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, BUSTED_PLAYER)).isEqualTo(PLAYER_LOSE); + } + + @DisplayName("fromDealerAndPlayer: 딜러가 버스트 패배") + @Test + void fromDealerAndPlayer_DealerBustLose() { + assertThat(fromDealerAndPlayer(BUSTED_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("fromDealerAndPlayer: 플레이어가 점수를 비교하여 승리") + @Test + void fromDealerAndPlayer_PlayerScoreWin() { + assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("fromDealerAndPlayer: 점수를 비교하여 무승부") + @Test + void fromDealerAndPlayer_ScoreDraw() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(DRAW); + } + + @DisplayName("fromDealerAndPlayer: 딜러가 점수를 비교하여 승리") + @Test + void fromDealerAndPlayer_DealerScoreWin() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, LOW_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); + } + + @DisplayName("calculateProfit: 플레이어가 블랙잭으로 승리했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsBlackjackWin() { + assertThat(PLAYER_WIN.calculateProfit(BLACKJACK_PLAYER)).isEqualTo(BigDecimal.valueOf(1_500_000)); + } + + @DisplayName("calculateProfit: 플레이어가 승리했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsWin() { + assertThat(PLAYER_WIN.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(1_000_000)); + + } + + @DisplayName("calculateProfit: 무승부일 때 수익 계산") + @Test + void calculateProfit_Draw() { + assertThat(DRAW.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(0)); + + } + + @DisplayName("calculateProfit: 플레이어가 패배했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsLose() { + assertThat(PLAYER_LOSE.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(-1 * 1_000_000)); + } +} diff --git a/src/test/java/domain/result/ParticipantProfitTest.java b/src/test/java/domain/result/ParticipantProfitTest.java new file mode 100644 index 00000000000..b7e56b47a4c --- /dev/null +++ b/src/test/java/domain/result/ParticipantProfitTest.java @@ -0,0 +1,43 @@ +package domain.result; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.participant.Dealer; +import domain.participant.Participant; + +public class ParticipantProfitTest { + @DisplayName("constructor: 인스턴스 생성") + @Test + void constructor() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + + assertThat(new ParticipantProfit(participant, profit)).isInstanceOf(ParticipantProfit.class); + } + + @DisplayName("getName: name을 반환") + @Test + void getName() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + ParticipantProfit participantProfit = new ParticipantProfit(participant, profit); + + assertThat(participantProfit.getName()).isNotNull(); + } + + @DisplayName("getProfit: profit을 반환") + @Test + void getProfit() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + + ParticipantProfit participantProfit = new ParticipantProfit(participant, profit); + + assertThat(participantProfit.getProfit()).isNotNull(); + } +} diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29bb2d..00000000000