diff --git a/README.md b/README.md index 1ff5f7b6790..af839f6af37 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ # java-blackjack -블랙잭 미션 저장소 +## 기능목록 + +### 이름 입력 + +- [x] 이름들을 입력받는다. +- 예외 + - [x] 중복된 이름인 경우 + +### 카드 2장씩 분배 후 공개 + +- [X] 참가자별로 모두 2장씩 분배한다. +- [X] 딜러는 첫 번째 카드만 공개한다. +- [X] 플레이어는 두 장 모두 공개한다. + +### 참가자 카드 추가 분배 + +- [x] 플레이어의 카드 총합이 21미만인 경우, 한 장 더 받을 지 물어본다. + - [x] y 의 경우, 한 장 더 분배한다. + - [x] 지금까지 받은 카드를 공개한다. + - [x] n 의 경우, 턴을 종료한다. +- [x] 딜러는 카드의 합을 계산한다. + - [x] 16 이하의 경우, 한 장 더 분배한다. + - [x] 17 이상의 경우, 턴을 종료한다. + +### 참가자 카드 공개 + +- [x] 참가자 모두의 카드를 공개한다. +- [x] 참가자가 지닌 카드의 합을 계산하여 공개한다. + +### 승패 결과 출력 + +- [x] 딜러와 각 플레이어 사이의 승패를 계산한다. + - 딜러와 플레이어가 둘 다 버스트이면, 딜러 승 + - 딜러나 플레이어 둘 중 한명이 버스트이면, 버스트가 아닌 사람이 승 + - 딜러와 플레이어가 둘 다 버스트가 아닌 상황 + - 카드 합이 같다면 무승부 + - 카드 합이 다르다면 합이 큰 사람이 승 +- [x] 딜러는 `0승 0무 0패` 형식으로 결과를 출력한다. +- [x] 플레이어는 `승` or `무` or `패` 형식으로 결과를 출력한다. + +## 블랙잭 도메인 용어 +- 딜러(Dealer) : 카드를 나눠주고 게임을 진행하는 역할 +- 핸드(Hand) : 플레이어나 딜러가 받은 카드 세트 +- 히트(Hit) : 추가 카드를 요청하는 행동 +- 버스트(Bust) : 카드 합계가 21을 초과하여 패배하는 상황 +- 푸시(Push) : 플레이어와 딜러의 점수가 같아 비기는 상황 +- 하드 핸드(Hard Hand) : 에이스가 없거나 에이스를 1로만 계산해야 하는 핸드 diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..07ac0280fa8 --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,19 @@ +package blackjack; + +import blackjack.controller.BlackjackController; +import blackjack.view.InputView; +import blackjack.view.ResultView; + +public class Application { + + public static void main(String[] args) { + final BlackjackController blackjackController = makeController(); + blackjackController.run(); + } + + private static BlackjackController makeController() { + final InputView inputView = new InputView(); + final ResultView resultView = new ResultView(); + return new BlackjackController(inputView, resultView); + } +} diff --git a/src/main/java/blackjack/controller/BlackjackController.java b/src/main/java/blackjack/controller/BlackjackController.java new file mode 100644 index 00000000000..65bbf7e868f --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,102 @@ +package blackjack.controller; + +import blackjack.domain.DealerWinningResult; +import blackjack.domain.card.Deck; +import blackjack.domain.card.Hand; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Gamer; +import blackjack.domain.participant.Player; +import blackjack.domain.participant.Players; +import blackjack.domain.shuffle.ShuffleCardGenerator; +import blackjack.util.StringParser; +import blackjack.view.InputView; +import blackjack.view.ResultView; +import java.util.List; +import java.util.Map; + +public class BlackjackController { + + private static final int DEALER_COUNT = 1; + private static final int SPREAD_CARD_SIZE_PER_PLAYER = 2; + private static final int SPREAD_SIZE = 1; + + private static final String YES = "y"; + private static final String NO = "n"; + + private final InputView inputView; + private final ResultView resultView; + + public BlackjackController(InputView inputView, ResultView resultView) { + this.inputView = inputView; + this.resultView = resultView; + } + + public void run() { + final Deck deck = new Deck(new ShuffleCardGenerator()); + final Dealer dealer = Dealer.createEmpty(); + final Players players = makePlayers(); + + spreadInitialCards(dealer, players, deck); + spreadExtraCards(dealer, players, deck); + showParticipantScore(dealer, players); + showWinningResult(dealer, players); + } + + private void showParticipantScore(final Dealer dealer, final Players players) { + resultView.makeParticipantsWithScoreMessage(Map.entry(dealer.getNickname(), dealer.showAllCards()), + players.showTotalCards()); + } + + private Players makePlayers() { + String names = inputView.readNames(); + List parsedNames = StringParser.parseByComma(names); + return new Players(parsedNames.stream() + .map(Player::createEmpty) + .toList()); + } + + private void spreadInitialCards(final Dealer dealer, final Players players, final Deck deck) { + final int cardsCount = (DEALER_COUNT + players.getSize()) * SPREAD_CARD_SIZE_PER_PLAYER; + final Hand hand = deck.spreadCards(cardsCount); + dealer.receiveCards(hand.getPartialCards(0, SPREAD_CARD_SIZE_PER_PLAYER)); + players.receiveCards(hand.getPartialCards(SPREAD_CARD_SIZE_PER_PLAYER, hand.getSize()), + SPREAD_CARD_SIZE_PER_PLAYER); + + resultView.printSpreadCard(players.getNames(), Map.entry(dealer.getNickname(), dealer.showInitialCards()), + players.showTotalInitialCards()); + } + + private void spreadExtraCards(final Dealer dealer, final Players players, final Deck deck) { + Players availablePlayers = players.findHitAvailablePlayers(); + for (Gamer gamer : availablePlayers.getPlayers()) { + while (gamer.canHit() && wantHit(gamer)) { + final Hand hand = deck.spreadCards(SPREAD_SIZE); + gamer.receiveCards(new Hand(List.of(hand.getFirstCard()))); + resultView.printParticipantTotalCards(gamer.getNickname(), gamer.showAllCards()); + } + } + while (dealer.canHit()) { + final Hand hand = deck.spreadCards(SPREAD_SIZE); + dealer.receiveCards(new Hand(List.of(hand.getFirstCard()))); + resultView.printDealerExtraCard(); + } + } + + private boolean wantHit(final Gamer player) { + String answer = inputView.askHit(player); + if (isValidAnswer(answer)) { + return answer.equals(YES); + } + resultView.showln("잘못된 응답입니다. 다시 입력해주세요."); + return wantHit(player); + } + + private boolean isValidAnswer(final String answer) { + return answer.equals(YES) || answer.equals(NO); + } + + private void showWinningResult(final Dealer dealer, final Players players) { + final DealerWinningResult result = dealer.makeDealerWinningResult(players.calculateScores()); + resultView.showDealerWinningResult(result); + } +} diff --git a/src/main/java/blackjack/domain/DealerWinningResult.java b/src/main/java/blackjack/domain/DealerWinningResult.java new file mode 100644 index 00000000000..a057a0af4d9 --- /dev/null +++ b/src/main/java/blackjack/domain/DealerWinningResult.java @@ -0,0 +1,41 @@ +package blackjack.domain; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +public class DealerWinningResult { + + private final Map result; + + public DealerWinningResult(final Map result) { + this.result = new HashMap<>(result); + } + + public int countResultStatus(final ResultStatus input) { + return (int) result.values().stream() + .filter(resultStatus -> resultStatus == input) + .count(); + } + + public Map makePlayerWinningResult() { + return result.entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, + entry -> entry.getValue().makeOppositeResult())); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final DealerWinningResult that)) { + return false; + } + return Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hashCode(result); + } +} diff --git a/src/main/java/blackjack/domain/ResultStatus.java b/src/main/java/blackjack/domain/ResultStatus.java new file mode 100644 index 00000000000..4c01b1ff500 --- /dev/null +++ b/src/main/java/blackjack/domain/ResultStatus.java @@ -0,0 +1,45 @@ +package blackjack.domain; + +import static blackjack.domain.card.Hand.BURST_THRESHOLD; + +public enum ResultStatus { + + WIN, + LOSE, + PUSH; + + public static ResultStatus calculateResultStatus(final int score, final int comparedScore) { + if (score <= BURST_THRESHOLD) { + return calculateResultStatusUnderBlackjackNumber(score, comparedScore); + } + return WIN; + } + + private static ResultStatus calculateResultStatusUnderBlackjackNumber(final int score, final int comparedScore) { + if (comparedScore <= BURST_THRESHOLD) { + return calculateResultStatusBothUnderBlackjackNumber(score, comparedScore); + } + return LOSE; + } + + private static ResultStatus calculateResultStatusBothUnderBlackjackNumber(final int score, + final int comparedScore) { + if (score > comparedScore) { + return LOSE; + } + if (score == comparedScore) { + return PUSH; + } + return WIN; + } + + public ResultStatus makeOppositeResult() { + if (this == WIN) { + return LOSE; + } + if (this == LOSE) { + return WIN; + } + return PUSH; + } +} 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..a2a3f9da53a --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,47 @@ +package blackjack.domain.card; + +import java.util.Objects; + +public class Card { + + private final Shape shape; + private final CardScore cardScore; + + public Card(final Shape shape, final CardScore cardScore) { + this.shape = shape; + this.cardScore = cardScore; + } + + public boolean isAce() { + return this.cardScore == CardScore.A; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Card card)) { + return false; + } + return shape == card.shape && cardScore == card.cardScore; + } + + @Override + public int hashCode() { + return Objects.hash(shape, cardScore); + } + + public Shape getShape() { + return shape; + } + + public String getCardScoreName() { + return cardScore.getName(); + } + + public int getCardMinNumber() { + return cardScore.getMinNumber(); + } + + public int getCardMaxNumber() { + return cardScore.getMaxNumber(); + } +} diff --git a/src/main/java/blackjack/domain/card/CardScore.java b/src/main/java/blackjack/domain/card/CardScore.java new file mode 100644 index 00000000000..4f15c56bbfc --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardScore.java @@ -0,0 +1,47 @@ +package blackjack.domain.card; + +import java.util.Collections; +import java.util.List; + +public enum CardScore { + + A(List.of(1, 11)), + TWO(List.of(2)), + THREE(List.of(3)), + FOUR(List.of(4)), + FIVE(List.of(5)), + SIX(List.of(6)), + SEVEN(List.of(7)), + EIGHT(List.of(8)), + NINE(List.of(9)), + TEN(List.of(10)), + K(List.of(10)), + Q(List.of(10)), + J(List.of(10)); + + private static final List SPECIAL_CARD_SCORE = List.of(CardScore.A, CardScore.K, + CardScore.Q, CardScore.J); + + private final List scores; + + CardScore(final List scores) { + this.scores = scores; + } + + public String getName() { + final boolean isSpecialCardScore = SPECIAL_CARD_SCORE.stream() + .anyMatch(cardScore -> cardScore == this); + if (isSpecialCardScore) { + return this.name(); + } + return String.valueOf(scores.getFirst()); + } + + public int getMinNumber() { + return Collections.min(scores); + } + + public int getMaxNumber() { + return Collections.max(scores); + } +} 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..0e2d84c0069 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +import blackjack.domain.shuffle.CardGenerator; +import java.util.stream.IntStream; + +public class Deck { + + private final CardGenerator cardGenerator; + + public Deck(final CardGenerator cardGenerator) { + this.cardGenerator = cardGenerator; + } + + public Hand spreadCards(final int count) { + return new Hand(IntStream.range(0, count) + .mapToObj(o -> cardGenerator.pickCard()) + .toList()); + } +} 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..fdd27163d0f --- /dev/null +++ b/src/main/java/blackjack/domain/card/Hand.java @@ -0,0 +1,89 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Hand { + + public static final int BURST_THRESHOLD = 21; + private static final int ACE_SUBTRACT = 10; + + private final List hand; + + public Hand(final List hand) { + this.hand = new ArrayList<>(hand); + } + + public int calculateResult() { + int maxScore = calculateMaxScore(hand); + if (isNotBurst(maxScore)) { + return maxScore; + } + return subtractAce(maxScore); + } + + public int calculateWithHardHand() { + return hand.stream() + .mapToInt(Card::getCardMinNumber) + .sum(); + } + + public void addAll(final Hand givenHand) { + hand.addAll(givenHand.getHand()); + } + + private int calculateMaxScore(final List cards) { + return cards.stream() + .mapToInt(Card::getCardMaxNumber) + .sum(); + } + + private boolean isNotBurst(final int score) { + return score <= BURST_THRESHOLD; + } + + private int subtractAce(int score) { + int aceCount = countAce(hand); + while (!isNotBurst(score) && aceCount-- > 0) { + score -= ACE_SUBTRACT; + } + return score; + } + + private int countAce(final List cards) { + return (int) cards.stream() + .filter(Card::isAce) + .count(); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Hand hand1)) { + return false; + } + return Objects.equals(hand, hand1.hand); + } + + @Override + public int hashCode() { + return Objects.hashCode(hand); + } + + public List getHand() { + return Collections.unmodifiableList(hand); + } + + public Hand getPartialCards(int start, int end) { + return new Hand(hand.subList(start, end)); + } + + public Card getFirstCard() { + return hand.getFirst(); + } + + public int getSize() { + return hand.size(); + } +} 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..8244f4038ec --- /dev/null +++ b/src/main/java/blackjack/domain/card/Shape.java @@ -0,0 +1,9 @@ +package blackjack.domain.card; + +public enum Shape { + + SPADE, + DIAMOND, + HEART, + CLOB; +} 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..be0609cd3c9 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,62 @@ +package blackjack.domain.participant; + +import blackjack.domain.ResultStatus; +import blackjack.domain.DealerWinningResult; +import blackjack.domain.card.Hand; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Dealer extends Gamer { + + private static final String NICKNAME = "딜러"; + private static final int DEALER_THRESHOLD = 16; + + public Dealer(final Hand hand) { + super(hand); + } + + public static Dealer createEmpty() { + return new Dealer(new Hand(new ArrayList<>())); + } + + @Override + public Hand showInitialCards() { + return new Hand(List.of(hand.getFirstCard())); + } + + @Override + public boolean canHit() { + int score = hand.calculateWithHardHand(); + return score <= DEALER_THRESHOLD; + } + + public DealerWinningResult makeDealerWinningResult(final Map playerScores) { + int dealerScore = calculateScore(); + Map result = playerScores.entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, + entry -> ResultStatus.calculateResultStatus(entry.getValue(), dealerScore))); + return new DealerWinningResult(result); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Dealer dealer)) { + return false; + } + return Objects.equals(hand, dealer.hand); + } + + @Override + public int hashCode() { + return Objects.hashCode(hand); + } + + @Override + public String getNickname() { + return NICKNAME; + } +} diff --git a/src/main/java/blackjack/domain/participant/Gamer.java b/src/main/java/blackjack/domain/participant/Gamer.java new file mode 100644 index 00000000000..49428c64585 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Gamer.java @@ -0,0 +1,30 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Hand; + +public abstract class Gamer { + + protected final Hand hand; + + public Gamer(final Hand hand) { + this.hand = hand; + } + + public abstract Hand showInitialCards(); + + public abstract boolean canHit(); + + public abstract String getNickname(); + + public void receiveCards(final Hand givenHand) { + hand.addAll(givenHand); + } + + public int calculateScore() { + return hand.calculateResult(); + } + + public Hand showAllCards() { + return hand; + } +} diff --git a/src/main/java/blackjack/domain/participant/Participants.java b/src/main/java/blackjack/domain/participant/Participants.java new file mode 100644 index 00000000000..511ad64703a --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participants.java @@ -0,0 +1,89 @@ +package blackjack.domain.participant; + +import blackjack.domain.DealerWinningResult; +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class Participants { + + private static final int DEALER_COUNT = 1; + private static final int SPREAD_MULTIPLY_SIZE = 2; + private static final int SPREAD_SIZE = 1; + + private final Dealer dealer; + private final Players players; + + public Participants(final Dealer dealer, final Players players) { + this.dealer = dealer; + this.players = players; + } + +// public void spreadAllTwoCards(final Hand hand) { +// dealer.receiveCards(hand.getPartialCards(0, SPREAD_MULTIPLY_SIZE)); +// players.receiveCards(hand.getPartialCards(SPREAD_MULTIPLY_SIZE, hand.getSize()), SPREAD_MULTIPLY_SIZE); +// } + +// public int getPlayerSize() { +// return players.getSize(); +// } +// +// public Entry showInitialDealerCards() { +// return Map.entry(dealer.getNickname(), dealer.showInitialCards()); +// } +// +// public Map showInitialParticipantsCards() { +// return players.showTotalInitialCards(); +// } + +// public boolean canDealerHit() { +// return dealer.canHit(); +// } + +// public boolean canPlayerHit(final int index) { +// final Gamer player = players.getPlayer(index); +// return player.canHit(); +// } + +// public Players findHitAvailablePlayers() { +// return players.findHitAvailablePlayers(); +// } +// +// public void spreadOneCardToDealer(final Card card) { +// spreadOneCard(dealer, card); +// } +// +// public Entry showDealerCards() { +// return Map.entry(dealer.getNickname(), dealer.showAllCards()); +// } +// +// public Map showPlayersCards() { +// return players.showTotalCards(); +// } +// +// public DealerWinningResult makeDealerWinningResult() { +// return dealer.makeDealerWinningResult(players.calculateScores()); +// } + +// public int getInitialTotalCardsSize() { +// return (DEALER_COUNT + players.getSize()) * SPREAD_MULTIPLY_SIZE; +// } +// +// private void spreadOneCard(final Gamer gamer, final Card card) { +// gamer.receiveCards(new Hand(List.of(card))); +// } + +// public Gamer getDealer() { +// return dealer; +// } + +// public List getPlayersNames() { +// return players.getNames(); +// } + +// public Players getPlayers() { +// return players; +// } +} 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..788c0f11b79 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,50 @@ +package blackjack.domain.participant; + +import static blackjack.domain.card.Hand.BURST_THRESHOLD; + +import blackjack.domain.card.Hand; +import java.util.ArrayList; +import java.util.Objects; + +public class Player extends Gamer { + + private final String nickname; + + public Player(final String nickname, final Hand hand) { + super(hand); + this.nickname = nickname; + } + + public static Player createEmpty(final String nickname) { + return new Player(nickname, new Hand(new ArrayList<>())); + } + + @Override + public Hand showInitialCards() { + return hand; + } + + @Override + public boolean canHit() { + int score = hand.calculateWithHardHand(); + return score < BURST_THRESHOLD; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Player player)) { + return false; + } + return Objects.equals(nickname, player.nickname) && Objects.equals(hand, player.hand); + } + + @Override + public int hashCode() { + return Objects.hash(nickname, hand); + } + + @Override + public String getNickname() { + return nickname; + } +} 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..83acc4a66ed --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Players.java @@ -0,0 +1,94 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Hand; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Players { + + private final List players; + + public Players(final List players) { + validate(players); + this.players = new ArrayList<>(players); + } + + public void receiveCards(final Hand hand, final int count) { + for (int i = 0; i < players.size(); i++) { + final Gamer player = players.get(i); + player.receiveCards(hand.getPartialCards(i * count, (i + 1) * count)); + } + } + + public Map showTotalInitialCards() { + return players.stream() + .collect(Collectors.toMap(Gamer::getNickname, + Gamer::showInitialCards, (e1, e2) -> e1, + LinkedHashMap::new)); + } + + public Map showTotalCards() { + return players.stream() + .collect(Collectors.toMap(Gamer::getNickname, Gamer::showInitialCards, (e1, e2) -> e1, + LinkedHashMap::new)); + } + + public Players findHitAvailablePlayers() { + return new Players(players.stream() + .filter(Gamer::canHit) + .toList()); + } + + public Map calculateScores() { + return players.stream() + .collect(Collectors.toMap(Gamer::getNickname, Gamer::calculateScore)); + } + + private void validate(List players) { + if (isDuplicate(players)) { + throw new IllegalArgumentException("[ERROR] 중복된 이름을 입력했습니다."); + } + } + + private boolean isDuplicate(List players) { + return players.size() != players.stream() + .distinct() + .count(); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Players players1)) { + return false; + } + return Objects.equals(players, players1.players); + } + + @Override + public int hashCode() { + return Objects.hashCode(players); + } + + public List getNames() { + return players.stream() + .map(Gamer::getNickname) + .toList(); + } + + public int getSize() { + return players.size(); + } + + public List getPlayers() { + return Collections.unmodifiableList(players); + } + + public Gamer getPlayer(final int index) { + return players.get(index); + } +} diff --git a/src/main/java/blackjack/domain/shuffle/CardGenerator.java b/src/main/java/blackjack/domain/shuffle/CardGenerator.java new file mode 100644 index 00000000000..bd1cb6ef324 --- /dev/null +++ b/src/main/java/blackjack/domain/shuffle/CardGenerator.java @@ -0,0 +1,17 @@ +package blackjack.domain.shuffle; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardScore; +import blackjack.domain.card.Shape; +import java.util.Arrays; +import java.util.List; + +public interface CardGenerator { + + List DECK = Arrays.stream(Shape.values()) + .flatMap(shape -> Arrays.stream(CardScore.values()) + .map(cardScore -> new Card(shape, cardScore))) + .toList(); + + Card pickCard(); +} diff --git a/src/main/java/blackjack/domain/shuffle/ShuffleCardGenerator.java b/src/main/java/blackjack/domain/shuffle/ShuffleCardGenerator.java new file mode 100644 index 00000000000..9bef9e13d55 --- /dev/null +++ b/src/main/java/blackjack/domain/shuffle/ShuffleCardGenerator.java @@ -0,0 +1,27 @@ +package blackjack.domain.shuffle; + +import blackjack.domain.card.Card; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +public class ShuffleCardGenerator implements CardGenerator { + + private final List deck = shuffleDeck(); + + @Override + public Card pickCard() { + try { + return deck.removeFirst(); + } catch (NoSuchElementException | UnsupportedOperationException exception) { + throw new IllegalStateException("[ERROR] 카드가 더이상 없습니다."); + } + } + + private List shuffleDeck() { + final List cards = new ArrayList<>(DECK); + Collections.shuffle(cards); + return cards; + } +} diff --git a/src/main/java/blackjack/util/StringParser.java b/src/main/java/blackjack/util/StringParser.java new file mode 100644 index 00000000000..3d8098a3c3a --- /dev/null +++ b/src/main/java/blackjack/util/StringParser.java @@ -0,0 +1,18 @@ +package blackjack.util; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.PatternSyntaxException; + +public class StringParser { + + private static final String COMMA = ","; + + public static List parseByComma(String input) { + try { + return Arrays.asList(input.replace(" ", "").split(COMMA)); + } catch (PatternSyntaxException exception) { + throw new IllegalArgumentException("[ERROR] 입력 형식이 잘못되었습니다."); + } + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..9e78ce177e6 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,26 @@ +package blackjack.view; + +import blackjack.domain.participant.Gamer; +import java.util.Scanner; + +public class InputView { + + private static final Scanner SCANNER = new Scanner(System.in); + private static final String TITLE_INPUT_NAMES = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"; + private static final String TITLE_ASK_MORE_CARD = "%s는 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)"; + private static final String LINE = System.lineSeparator(); + + public String readNames() { + System.out.println(TITLE_INPUT_NAMES); + return readLine(); + } + + public String askHit(Gamer player) { + System.out.printf(TITLE_ASK_MORE_CARD + LINE, player.getNickname()); + return readLine(); + } + + private String readLine() { + return SCANNER.nextLine(); + } +} diff --git a/src/main/java/blackjack/view/ResultView.java b/src/main/java/blackjack/view/ResultView.java new file mode 100644 index 00000000000..9b59c6eaaab --- /dev/null +++ b/src/main/java/blackjack/view/ResultView.java @@ -0,0 +1,118 @@ +package blackjack.view; + +import blackjack.domain.DealerWinningResult; +import blackjack.domain.ResultStatus; +import blackjack.domain.card.Hand; +import blackjack.domain.card.Shape; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class ResultView { + + private static final Map SHAPE_KOREAN = Map.of( + Shape.SPADE, "스페이드", + Shape.DIAMOND, "다이아몬드", + Shape.HEART, "하트", + Shape.CLOB, "클로버" + ); + private static final Map RESULT_STATUS_KOREAN = Map.of( + ResultStatus.WIN, "승", + ResultStatus.LOSE, "패", + ResultStatus.PUSH, "무" + ); + private static final String LINE = System.lineSeparator(); + private static final String NAME_FORMAT = "딜러와 %s에게 2장을 나누었습니다."; + private static final String COMMA = ", "; + private static final String CARD_FORMAT = "%s카드: %s"; + private static final String TITLE_DEALER_EXTRA_CARD = "딜러는 16이하라 한장의 카드를 더 받았습니다."; + private static final String CARD_SCORE_FORMAT = "%s카드: %s - 결과: %d"; + + private static final String TITLE_WINNING_RESULT = "## 최종 승패"; + private static final String WINNING_DEALER_RESULT_FORMAT = "딜러: %d승 %d무 %d패"; + private static final String WINNING_PLAYER_RESULT_FORMAT = "%s: %s"; + + public void printSpreadCard(final List names, final Entry dealerCards, + final Map playersCards) { + printNames(names); + printDealerCards(dealerCards); + printPlayersCards(playersCards); + } + + public void printParticipantTotalCards(final String nickname, final Hand hand) { + System.out.println(makeCardMessage(nickname, hand)); + } + + public void printDealerExtraCard() { + System.out.println(LINE + TITLE_DEALER_EXTRA_CARD); + } + + private void printNames(final List names) { + final String joinedNames = String.join(COMMA, names); + System.out.printf(LINE + NAME_FORMAT + LINE, joinedNames); + } + + public void printDealerCards(final Entry dealerCards) { + final String dealerMessage = makeCardMessage(dealerCards.getKey(), dealerCards.getValue()); + System.out.println(dealerMessage); + } + + public void printPlayersCards(final Map playersCards) { + playersCards.entrySet().stream() + .map(entry -> makeCardMessage(entry.getKey(), entry.getValue())) + .forEach(System.out::println); + System.out.println(); + } + + private String makeCardMessage(final String nickname, final Hand hand) { + return String.format(CARD_FORMAT, nickname, getCardMessage(hand)); + } + + private String getCardMessage(final Hand hand) { + return hand.getHand().stream() + .map(card -> card.getCardScoreName() + getShapeName(card.getShape())) + .collect(Collectors.joining(COMMA)); + } + + public void showDealerWinningResult(final DealerWinningResult result) { + System.out.println(LINE + TITLE_WINNING_RESULT); + int winCount = result.countResultStatus(ResultStatus.WIN); + int drawCount = result.countResultStatus(ResultStatus.PUSH); + int loseCount = result.countResultStatus(ResultStatus.LOSE); + System.out.printf(WINNING_DEALER_RESULT_FORMAT + LINE, winCount, drawCount, loseCount); + showPlayerResultStatus(result.makePlayerWinningResult()); + } + + public void showln(final String line) { + System.out.println(line); + } + + public void makeParticipantsWithScoreMessage(final Entry dealer, + final Map participants) { + System.out.println(LINE + makeParticipantsWithScoreMessage(dealer)); + participants.entrySet().stream() + .map(this::makeParticipantsWithScoreMessage) + .forEach(System.out::println); + } + + private void showPlayerResultStatus(final Map result) { + for (Entry entry : result.entrySet()) { + System.out.printf(WINNING_PLAYER_RESULT_FORMAT + LINE, entry.getKey(), + getResultStatusName(entry.getValue())); + } + } + + private String makeParticipantsWithScoreMessage(final Entry entry) { + return String.format(CARD_SCORE_FORMAT, entry.getKey(), + getCardMessage(entry.getValue()), entry.getValue().calculateResult()); + } + + private String getShapeName(final Shape shape) { + return SHAPE_KOREAN.get(shape); + } + + private String getResultStatusName(final ResultStatus resultStatus) { + return RESULT_STATUS_KOREAN.get(resultStatus); + } +} diff --git a/src/test/java/blackjack/domain/BlackjackGameTest.java b/src/test/java/blackjack/domain/BlackjackGameTest.java new file mode 100644 index 00000000000..020c2944b58 --- /dev/null +++ b/src/test/java/blackjack/domain/BlackjackGameTest.java @@ -0,0 +1,185 @@ +//package blackjack.domain; +// +// +//import static blackjack.fixture.TestFixture.provideEmptyCards; +//import static blackjack.fixture.TestFixture.provideOver16Cards; +//import static blackjack.fixture.TestFixture.provideOver21Cards; +//import static blackjack.fixture.TestFixture.provideParticipants; +//import static blackjack.fixture.TestFixture.providePlayers; +//import static blackjack.fixture.TestFixture.provideThreePlayersWithCards; +//import static blackjack.fixture.TestFixture.provideTwoPlayersWithCards; +//import static blackjack.fixture.TestFixture.provideUnder16Cards; +//import static blackjack.fixture.TestFixture.provideUnder21Cards; +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.junit.jupiter.api.Assertions.assertAll; +// +//import blackjack.domain.card.Card; +//import blackjack.domain.card.Deck; +//import blackjack.domain.card.CardScore; +//import blackjack.domain.card.Hand; +//import blackjack.domain.card.Shape; +//import blackjack.domain.participant.Dealer; +//import blackjack.domain.participant.Gamer; +//import blackjack.domain.participant.Participants; +//import blackjack.domain.participant.Players; +//import blackjack.domain.shuffle.ShuffleCardGenerator; +//import java.util.List; +//import java.util.Map; +//import java.util.stream.Stream; +//import org.junit.jupiter.api.Assertions; +//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 BlackjackGameTest { +// +// @DisplayName("플레이어 및 딜러에게 카드를 두 장씩 나눠준다.") +// @Test +// void spreadTwoCardsToDealerAndPlayers() { +// // given +// final BlackjackGame blackjackGame = new BlackjackGame( +// new Deck(new ShuffleCardGenerator()), +// provideParticipants()); +// // when +// blackjackGame.spreadInitialCards(); +// +// // then +// Assertions.assertAll( +// () -> assertThat(blackjackGame.showDealerCard().getValue().getHand()).hasSize(2), +// () -> assertThat(blackjackGame.showPlayersCards()).hasSize(2) +// ); +// } +// +// @DisplayName("플레이어가 카드를 받을 수 있는지 물어본다.") +// @Test +// void canPlayerMoreCardToPlayer() { +// // given +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// new Participants(new Dealer(provideEmptyCards()), +// new Players(provideTwoPlayersWithCards(provideUnder21Cards(), provideOver21Cards())))); +// +// // when & then +// assertAll( +// () -> assertThat(blackjackGame.canPlayerHit(0)).isTrue(), +// () -> assertThat(blackjackGame.canPlayerHit(1)).isFalse() +// ); +// } +// +// @DisplayName("한 장의 카드를 플레이어에게 준다.") +// @Test +// void spreadOneCardToPlayer() { +// // given +// Players players = providePlayers(); +// Participants participants = new Participants(new Dealer(provideEmptyCards()), players); +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// participants); +// Gamer gamer = players.getPlayer(0); +// +// // when +// blackjackGame.spreadOneCardToPlayer(gamer); +// +// // then +// assertThat(gamer.showAllCards().getHand()).hasSize(1); +// } +// +// @DisplayName("딜러가 카드를 더 받을 수 있는지 확인한다.") +// @ParameterizedTest +// @MethodSource +// void canDealerHit(final Hand hand, final boolean expected) { +// // given +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// new Participants(new Dealer(hand), +// new Players(provideTwoPlayersWithCards(provideEmptyCards(), provideEmptyCards())))); +// +// // when & then +// assertThat(blackjackGame.canDealerHit()).isEqualTo(expected); +// } +// +// private static Stream canDealerHit() { +// return Stream.of( +// Arguments.of(provideUnder16Cards(), true), +// Arguments.of(provideOver16Cards(), false) +// ); +// } +// +// @DisplayName("플레이어가 카드를 더 받을 수 있는지 확인한다.") +// @ParameterizedTest +// @MethodSource +// void canPlayerHit(final Hand hand, final boolean expected) { +// // given +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// new Participants(new Dealer(provideEmptyCards()), +// new Players(provideTwoPlayersWithCards(hand, provideEmptyCards())))); +// +// // when & then +// assertThat(blackjackGame.canPlayerHit(0)).isEqualTo(expected); +// } +// +// private static Stream canPlayerHit() { +// return Stream.of( +// Arguments.of(provideUnder21Cards(), true), +// Arguments.of(provideOver21Cards(), false) +// ); +// } +// +// @DisplayName("카드의 합을 계산한다.") +// @Test +// void calculateScore() { +// // given +// final Dealer dealer = new Dealer(provideUnder16Cards()); +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// new Participants(dealer, +// new Players(provideTwoPlayersWithCards(provideUnder21Cards(), provideOver21Cards())))); +// +// // when & then +// assertAll( +// () -> assertThat(blackjackGame.calculateScore(dealer)).isEqualTo(6), +// () -> assertThat(blackjackGame.calculateScore(blackjackGame.getPlayer(0))).isEqualTo(5), +// () -> assertThat(blackjackGame.calculateScore(blackjackGame.getPlayer(1))).isEqualTo(30) +// ); +// } +// +// @DisplayName("딜러의 승,패 결과를 계산한다") +// @Test +// void makeDealerWinningResult() { +// // given +// final Dealer dealer = new Dealer(new Hand(List.of(new Card(Shape.SPADE, CardScore.J), +// new Card(Shape.SPADE, CardScore.EIGHT)))); +// final Hand player1Hand = new Hand(List.of(new Card(Shape.SPADE, CardScore.NINE), +// new Card(Shape.SPADE, CardScore.TEN))); +// final Hand player2Hand = new Hand(List.of(new Card(Shape.SPADE, CardScore.TWO), +// new Card(Shape.SPADE, CardScore.J))); +// final Hand player3Hand = new Hand(List.of(new Card(Shape.SPADE, CardScore.EIGHT), +// new Card(Shape.SPADE, CardScore.K))); +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// new Participants(dealer, +// new Players(provideThreePlayersWithCards(player1Hand, player2Hand, player3Hand)))); +// +// final DealerWinningResult expected = new DealerWinningResult(Map.of( +// "엠제이", ResultStatus.LOSE, "밍트", ResultStatus.WIN, "포비", ResultStatus.PUSH +// )); +// +// // when +// DealerWinningResult dealerWinningResult = blackjackGame.makeDealerWinningResult(); +// +// // then +// assertThat(dealerWinningResult).isEqualTo(expected); +// } +// +// @DisplayName("한 장의 카드를 딜러에게 준다.") +// @Test +// void spreadOneCardToDealer() { +// // given +// Participants participants = provideParticipants(); +// final BlackjackGame blackjackGame = new BlackjackGame(new Deck(new ShuffleCardGenerator()), +// participants); +// +// // when +// blackjackGame.spreadOneCardToDealer(); +// +// // then +// assertThat(participants.getDealer().showAllCards().getHand()).hasSize(1); +// } +//} diff --git a/src/test/java/blackjack/domain/DealerWinningResultTest.java b/src/test/java/blackjack/domain/DealerWinningResultTest.java new file mode 100644 index 00000000000..de77d085506 --- /dev/null +++ b/src/test/java/blackjack/domain/DealerWinningResultTest.java @@ -0,0 +1,45 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DealerWinningResultTest { + + @Test + void 딜러_우승_결과를_조회한다() { + // Given + DealerWinningResult winningResult = new DealerWinningResult( + Map.of("밍트", ResultStatus.WIN, "엠제이", ResultStatus.LOSE)); + + // When & then + assertAll( + () -> assertThat(winningResult.countResultStatus(ResultStatus.WIN)).isEqualTo(1), + () -> assertThat(winningResult.countResultStatus(ResultStatus.LOSE)).isEqualTo(1), + () -> assertThat(winningResult.countResultStatus(ResultStatus.PUSH)).isEqualTo(0) + ); + } + + @Test + void 플레이어의_우승_결과를_조회한다() { + // Given + String mint = "밍트"; + String mj = "엠제이"; + String pobi = "포비"; + DealerWinningResult winningResult = new DealerWinningResult( + Map.of(mint, ResultStatus.WIN, mj, ResultStatus.LOSE, pobi, ResultStatus.PUSH) + ); + + // When + Map playerWinningResult = winningResult.makePlayerWinningResult(); + + // Then + assertAll( + () -> assertThat(playerWinningResult.get(mint)).isEqualTo(ResultStatus.LOSE), + () -> assertThat(playerWinningResult.get(mj)).isEqualTo(ResultStatus.WIN), + () -> assertThat(playerWinningResult.get(pobi)).isEqualTo(ResultStatus.PUSH) + ); + } +} diff --git a/src/test/java/blackjack/domain/ResultStatusTest.java b/src/test/java/blackjack/domain/ResultStatusTest.java new file mode 100644 index 00000000000..7af1149d978 --- /dev/null +++ b/src/test/java/blackjack/domain/ResultStatusTest.java @@ -0,0 +1,27 @@ +package blackjack.domain; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ResultStatusTest { + + @DisplayName("두 개의 값으로 딜러의 승리 결과를 계산한다.") + @CsvSource({ + "20, 19, LOSE", + "20, 20, PUSH", + "20, 21, WIN", + "20, 25, LOSE", + "23, 25, WIN", + "23, 18, WIN" + }) + @ParameterizedTest + void calculateResultStatus(final int score, final int comparedScore, final ResultStatus resultStatus) { + // given + + // when & then + assertThat(ResultStatus.calculateResultStatus(score, comparedScore)).isEqualTo(resultStatus); + } +} diff --git a/src/test/java/blackjack/domain/card/CardScoreTest.java b/src/test/java/blackjack/domain/card/CardScoreTest.java new file mode 100644 index 00000000000..9fc80b556c1 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardScoreTest.java @@ -0,0 +1,17 @@ +package blackjack.domain.card; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardScoreTest { + + @Test + @DisplayName("이름을 조회한다") + void findName() { + // Given + + // When + + // Then + } +} 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..be71ee65f84 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,20 @@ +package blackjack.domain.card; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardTest { + + @DisplayName("카드를 생성한다.") + @Test + void test() { + // given + final Shape shape = Shape.DIAMOND; + final CardScore cardScore = CardScore.A; + + // when & then + Assertions.assertThatCode(() -> new Card(shape, cardScore)) + .doesNotThrowAnyException(); + } +} 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..0cc4d232671 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,52 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.domain.shuffle.CardGenerator; +import blackjack.domain.shuffle.ShuffleCardGenerator; +import blackjack.fixture.TestFixture.TestCardGeneratorGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DeckTest { + + private Deck deck; + + @BeforeEach + void setUp() { + deck = new Deck(new ShuffleCardGenerator()); + } + + @DisplayName("카드를 여러장 분배한다.") + @Test + void spreadCards() { + // given + final int count = 2; + + // when + final Hand hand = deck.spreadCards(count); + + // then + assertThat(hand.getHand()).hasSize(count); + } + + @DisplayName("사용한 카드를 뽑았을 경우 다시 뽑는다.") + @Test + void spreadAgain() { + // given + final CardGenerator cardGenerator = new TestCardGeneratorGenerator(); + final Deck deck = new Deck(cardGenerator); + + // when + final Card firstCard = deck.spreadCards(1).getFirstCard(); + final Card secondCard = deck.spreadCards(1).getFirstCard(); + + // then + assertAll( + () -> assertThat(firstCard).isEqualTo(new Card(Shape.DIAMOND, CardScore.TWO)), + () -> assertThat(secondCard).isEqualTo(new Card(Shape.DIAMOND, CardScore.THREE)) + ); + } +} 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..a862c22ba56 --- /dev/null +++ b/src/test/java/blackjack/domain/card/HandTest.java @@ -0,0 +1,51 @@ +package blackjack.domain.card; + +import static blackjack.fixture.TestFixture.provideBiggerAceCards; +import static blackjack.fixture.TestFixture.provideBiggerAndSmallerAceCards; +import static blackjack.fixture.TestFixture.provideSmallerAceCards; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HandTest { + + @DisplayName("21 이하에서 카드 최대 합을 구한다") + @ParameterizedTest + @MethodSource + void calculateMaxScore(final Hand hand, final int expected) { + // given + + // when & then + assertThat(hand.calculateResult()).isEqualTo(expected); + } + + private static Stream calculateMaxScore() { + return Stream.of( + Arguments.of(provideSmallerAceCards(), 18), + Arguments.of(provideBiggerAceCards(), 21), + Arguments.of(provideBiggerAndSmallerAceCards(), 17) + ); + } + + @DisplayName("21 이하에서 카드 최소 합을 구한다") + @ParameterizedTest + @MethodSource + void calculateWithHardHand(final Hand hand, final int expected) { + // given + + // when & then + assertThat(hand.calculateWithHardHand()).isEqualTo(expected); + } + + private static Stream calculateWithHardHand() { + return Stream.of( + Arguments.of(provideSmallerAceCards(), 18), + Arguments.of(provideBiggerAceCards(), 11), + Arguments.of(provideBiggerAndSmallerAceCards(), 7) + ); + } +} 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..3241d24ca9b --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,106 @@ +package blackjack.domain.participant; + +import static blackjack.fixture.TestFixture.provideBiggerAceCards; +import static blackjack.fixture.TestFixture.provideBiggerAndSmallerAceCards; +import static blackjack.fixture.TestFixture.provideCards; +import static blackjack.fixture.TestFixture.provideEmptyCards; +import static blackjack.fixture.TestFixture.provideSmallerAceCards; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.card.CardScore; +import blackjack.domain.card.Shape; +import java.util.List; +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; + +class DealerTest { + + private Dealer dealer; + + @BeforeEach + void setUp() { + dealer = new Dealer(provideEmptyCards()); + } + + @DisplayName("카드들을 받는다.") + @Test + void receiveCards() { + // given + final Hand hand = provideCards(2); + + // when + dealer.receiveCards(hand); + + // then + assertThat(dealer).isEqualTo(new Dealer(hand)); + } + + @DisplayName("딜러는 카드 2개 중 1개만 보여준다.") + @Test + void showDealerCards() { + // given + final Hand hand = provideCards(2); + dealer.receiveCards(hand); + + // when + final Hand dealerHand = dealer.showInitialCards(); + + // then + assertThat(dealerHand.getHand()).isEqualTo(List.of(new Card(Shape.SPADE, CardScore.A))); + } + + @DisplayName("딜러가 가진 카드의 합이 16 이하면 true를 반환한다.") + @ParameterizedTest + @MethodSource + void canHit(final Hand hand, final boolean expected) { + // given + final Dealer dealer = new Dealer(hand); + + // when & then + assertThat(dealer.canHit()).isEqualTo(expected); + } + + private static Stream canHit() { + return Stream.of( + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.TEN), + new Card(Shape.SPADE, CardScore.NINE) + )), false), + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.A), + new Card(Shape.SPADE, CardScore.K) + )), true), + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.K), + new Card(Shape.SPADE, CardScore.Q), + new Card(Shape.SPADE, CardScore.A) + )), false) + ); + } + + @DisplayName("카드 합을 구한다") + @ParameterizedTest + @MethodSource + void makeDealerWinningResult(final Hand hand, final int expected) { + // given + dealer.receiveCards(hand); + + // when & then + assertThat(dealer.calculateScore()).isEqualTo(expected); + } + + private static Stream makeDealerWinningResult() { + return Stream.of( + Arguments.of(provideSmallerAceCards(), 18), + Arguments.of(provideBiggerAceCards(), 21), + Arguments.of(provideBiggerAndSmallerAceCards(), 17) + ); + } +} diff --git a/src/test/java/blackjack/domain/participant/ParticipantsTest.java b/src/test/java/blackjack/domain/participant/ParticipantsTest.java new file mode 100644 index 00000000000..3e1c403a5ed --- /dev/null +++ b/src/test/java/blackjack/domain/participant/ParticipantsTest.java @@ -0,0 +1,95 @@ +//package blackjack.domain.participant; +// +//import static blackjack.fixture.TestFixture.provideCards; +//import static blackjack.fixture.TestFixture.provideOver16Cards; +//import static blackjack.fixture.TestFixture.provideOver21Cards; +//import static blackjack.fixture.TestFixture.provideParticipants; +//import static blackjack.fixture.TestFixture.provideTwoPlayersWithCards; +//import static blackjack.fixture.TestFixture.provideUnder21Cards; +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.junit.jupiter.api.Assertions.assertAll; +// +//import blackjack.domain.card.Hand; +//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 ParticipantsTest { +// +// @DisplayName("모든 참가자에게 카드 두장씩 분배한다.") +// @Test +// void test() { +// // given +// Participants participants = provideParticipants(); +// Hand hand = provideCards(6); +// final Hand totalHand = provideCards(6); +// final Hand dealerHand = totalHand.getPartialCards(0, 2); +// final Hand playerCard1 = totalHand.getPartialCards(2, 4); +// final Hand playerCard2 = totalHand.getPartialCards(4, 6); +// +// // when +// participants.spreadAllTwoCards(hand); +// +// // then +// assertAll( +// () -> assertThat(participants.getDealer()).isEqualTo(new Dealer(dealerHand)), +// () -> assertThat(participants.getPlayers()).isEqualTo( +// new Players(provideTwoPlayersWithCards(playerCard1, playerCard2))) +// ); +// } +// +// @DisplayName("초기에 나눠주는 카드 수를 계산한다.") +// @Test +// void calculateParticipantsSize() { +// // given +// Participants participants = provideParticipants(); +// +// // when & then +// assertThat(participants.getInitialTotalCardsSize()).isEqualTo(6); +// } +// +// @DisplayName("딜러가 카드를 더 얻을 수 있으면 true를 반환한다.") +// @ParameterizedTest +// @MethodSource +// void canDealerHit(final Dealer dealer, final boolean expected) { +// // given +// final Hand firstHand = provideUnder21Cards(); +// final Hand secondHand = provideOver21Cards(); +// final List players = provideTwoPlayersWithCards(firstHand, secondHand); +// Participants participants = new Participants(dealer, new Players(players)); +// +// // when & then +// assertThat(participants.canDealerHit()).isEqualTo(expected); +// } +// +// private static Stream canDealerHit() { +// return Stream.of( +// Arguments.of(Dealer.createEmpty(), true), +// Arguments.of(new Dealer(provideOver16Cards()), false) +// ); +// } +// +// @DisplayName("플레이어가 카드를 더 얻을 수 있으면 true를 반환한다.") +// @ParameterizedTest +// @MethodSource +// void canPlayerHit(final Hand hand, final boolean expected) { +// // given +// final Players players = new Players(List.of(new Player("밍트", hand))); +// +// Participants participants = new Participants(Dealer.createEmpty(), players); +// +// // when & then +// assertThat(participants.canPlayerHit(0)).isEqualTo(expected); +// } +// +// private static Stream canPlayerHit() { +// return Stream.of( +// Arguments.of(provideUnder21Cards(), true), +// Arguments.of(provideOver21Cards(), false) +// ); +// } +//} 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..b0927717ce8 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,137 @@ +package blackjack.domain.participant; + +import static blackjack.fixture.TestFixture.provideBiggerAceCards; +import static blackjack.fixture.TestFixture.provideBiggerAndSmallerAceCards; +import static blackjack.fixture.TestFixture.provideCards; +import static blackjack.fixture.TestFixture.provideEmptyCards; +import static blackjack.fixture.TestFixture.provideSmallerAceCards; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.card.CardScore; +import blackjack.domain.card.Shape; +import java.util.List; +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; + +public class PlayerTest { + + private Player player; + + @BeforeEach + void setUp() { + player = new Player("엠제이", provideEmptyCards()); + } + + @DisplayName("이름으로 Player 객체를 생성한다.") + @Test + void createAttendee() { + // given + String nickname = "pobi"; + + // when & then + assertThatCode(() -> new Player(nickname, provideEmptyCards())) + .doesNotThrowAnyException(); + } + + @DisplayName("카드들을 받는다.") + @Test + void receiveCards() { + // given + Hand hand = provideCards(2); + + // when + player.receiveCards(hand); + + // then + assertThat(player).isEqualTo(new Player("엠제이", hand)); + } + + @DisplayName("플레이어는 모든 카드를 보여준다.") + @Test + void showPlayerAllCards() { + // given + final Hand hand = provideCards(2); + player.receiveCards(hand); + final List expected = List.of(new Card(Shape.SPADE, CardScore.A), + new Card(Shape.SPADE, CardScore.TWO)); + + // when + final Hand playerHand = player.showAllCards(); + + // then + assertThat(playerHand.getHand()).isEqualTo(expected); + } + + @DisplayName("플레이어는 1장만 보여준다.") + @Test + void showPlayerInitialCards() { + // given + final Hand hand = provideCards(2); + player.receiveCards(hand); + final List expected = List.of(new Card(Shape.SPADE, CardScore.A), + new Card(Shape.SPADE, CardScore.TWO)); + + // when + final Hand playerHand = player.showInitialCards(); + + // then + assertThat(playerHand.getHand()).isEqualTo(expected); + } + + + @DisplayName("플레이어가 가진 카드의 합이 21 미만이면 true를 반환한다.") + @ParameterizedTest + @MethodSource + void canHit(final Hand hand, final boolean expected) { + // given + final Player player = new Player("엠제이", hand); + + // when & then + assertThat(player.canHit()).isEqualTo(expected); + } + + private static Stream canHit() { + return Stream.of( + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.TEN), + new Card(Shape.SPADE, CardScore.NINE) + )), true), + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.A), + new Card(Shape.SPADE, CardScore.K) + )), true), + Arguments.of(new Hand(List.of( + new Card(Shape.SPADE, CardScore.K), + new Card(Shape.SPADE, CardScore.Q), + new Card(Shape.SPADE, CardScore.A) + )), false) + ); + } + + @DisplayName("카드 합을 구한다") + @ParameterizedTest + @MethodSource + void calculateMaxScore(final Hand hand, final int expected) { + // given + player.receiveCards(hand); + + // when & then + assertThat(player.calculateScore()).isEqualTo(expected); + } + + private static Stream calculateMaxScore() { + return Stream.of( + Arguments.of(provideSmallerAceCards(), 18), + Arguments.of(provideBiggerAceCards(), 21), + Arguments.of(provideBiggerAndSmallerAceCards(), 17) + ); + } +} 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..84a84c55ef7 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayersTest.java @@ -0,0 +1,65 @@ +package blackjack.domain.participant; + +import static blackjack.fixture.TestFixture.provideCards; +import static blackjack.fixture.TestFixture.provideEmptyCards; +import static blackjack.fixture.TestFixture.providePlayers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.domain.card.Hand; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PlayersTest { + + private Players players; + + @BeforeEach + void setUp() { + players = providePlayers(); + } + + @DisplayName("이름들로 Player 객체를 생성한다.") + @Test + void createPlayers() { + // given + List players = List.of(new Player("엠제이", provideEmptyCards()), new Player("밍트", provideEmptyCards())); + + // when & then + Assertions.assertThatCode(() -> new Players(players)) + .doesNotThrowAnyException(); + } + + @DisplayName("중복된 이름의 경우 예외를 발생한다.") + @Test + void createDuplicatePlayers() { + // given + List players = List.of(new Player("엠제이", provideEmptyCards()), new Player("엠제이", provideEmptyCards())); + + // when & then + Assertions.assertThatThrownBy(() -> new Players(players)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 중복된 이름을 입력했습니다."); + } + + @DisplayName("플레이어들이 카드를 받는다.") + @Test + void receiveCards() { + // given + final Hand hand = provideCards(4); + final int count = 2; + + // when + players.receiveCards(hand, count); + + // then + assertAll( + () -> assertThat(players.getPlayers().getFirst()).isEqualTo( + new Player("엠제이", hand.getPartialCards(0, 2))), + () -> assertThat(players.getPlayers().get(1)).isEqualTo(new Player("밍트", hand.getPartialCards(2, 4))) + ); + } +} diff --git a/src/test/java/blackjack/domain/shuffle/CardRandomGeneratorTest.java b/src/test/java/blackjack/domain/shuffle/CardRandomGeneratorTest.java new file mode 100644 index 00000000000..60193f4037e --- /dev/null +++ b/src/test/java/blackjack/domain/shuffle/CardRandomGeneratorTest.java @@ -0,0 +1,46 @@ +package blackjack.domain.shuffle; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardRandomGeneratorTest { + + private CardGenerator cardGenerator; + + @BeforeEach + void setUp() { + cardGenerator = new ShuffleCardGenerator(); + } + + @DisplayName("카드를 랜덤으로 한장 뽑는다.") + @Test + void pickOneRandomCard() { + // given + + // when + final Card card = cardGenerator.pickCard(); + + // then + assertThat(card).isNotNull(); + } + + @Test + @DisplayName("카드가 더이상 없을 경우 예외가 발생한다.") + void pickRandomCardWhenEmpty() { + // Given + final Deck deck = new Deck(cardGenerator); + final int deckSize = 52; + deck.spreadCards(deckSize); + + // When & Then + Assertions.assertThatThrownBy(() -> cardGenerator.pickCard()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("[ERROR] 카드가 더이상 없습니다."); + } +} diff --git a/src/test/java/blackjack/fixture/TestFixture.java b/src/test/java/blackjack/fixture/TestFixture.java new file mode 100644 index 00000000000..1b679f6d3b9 --- /dev/null +++ b/src/test/java/blackjack/fixture/TestFixture.java @@ -0,0 +1,96 @@ +package blackjack.fixture; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardScore; +import blackjack.domain.card.Hand; +import blackjack.domain.card.Shape; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Participants; +import blackjack.domain.participant.Player; +import blackjack.domain.participant.Players; +import blackjack.domain.shuffle.CardGenerator; +import java.util.ArrayList; +import java.util.List; + +public class TestFixture { + + public static class TestCardGeneratorGenerator implements CardGenerator { + + private int callingCount = 0; + + @Override + public Card pickCard() { + callingCount += 1; + if (callingCount == 0 || callingCount == 1) { + return new Card(Shape.DIAMOND, CardScore.TWO); + } + return new Card(Shape.DIAMOND, CardScore.THREE); + } + } + + public static Participants provideParticipants() { + return new Participants(new Dealer(provideEmptyCards()), providePlayers()); + } + + public static Hand provideCards(final int count) { + return new Hand(CardGenerator.DECK.subList(0, count)); + } + + public static Players providePlayers() { + return new Players(List.of(new Player("엠제이", provideEmptyCards()), new Player("밍트", provideEmptyCards()))); + } + + public static List provideTwoPlayersWithCards(final Hand hand1, final Hand hand2) { + return List.of(new Player("엠제이", hand1), new Player("밍트", hand2)); + } + + public static List provideThreePlayersWithCards(final Hand hand1, final Hand hand2, + final Hand hand3) { + return List.of(new Player("엠제이", hand1), new Player("밍트", hand2), new Player("포비", hand3)); + } + + public static Hand provideOver21Cards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.K), + new Card(Shape.SPADE, CardScore.Q), new Card(Shape.SPADE, CardScore.J))); + } + + public static Hand provideUnder21Cards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.TWO), + new Card(Shape.SPADE, CardScore.THREE))); + } + + public static Hand provideUnder16Cards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.TWO), + new Card(Shape.SPADE, CardScore.FOUR))); + } + + public static Hand provideOver16Cards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.TEN), + new Card(Shape.HEART, CardScore.TEN))); + } + + public static Hand provideSmallerAceCards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.EIGHT), + new Card(Shape.HEART, CardScore.NINE), + new Card(Shape.HEART, CardScore.A) + )); + } + + public static Hand provideBiggerAceCards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.K), + new Card(Shape.HEART, CardScore.A) + )); + } + + public static Hand provideBiggerAndSmallerAceCards() { + return new Hand(List.of(new Card(Shape.SPADE, CardScore.A), + new Card(Shape.HEART, CardScore.A), + new Card(Shape.HEART, CardScore.TWO), + new Card(Shape.HEART, CardScore.THREE) + )); + } + + public static Hand provideEmptyCards() { + return new Hand(new ArrayList<>()); + } +} diff --git a/src/test/java/blackjack/util/StringParserTest.java b/src/test/java/blackjack/util/StringParserTest.java new file mode 100644 index 00000000000..4b96148fb6d --- /dev/null +++ b/src/test/java/blackjack/util/StringParserTest.java @@ -0,0 +1,21 @@ +package blackjack.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class StringParserTest { + + @DisplayName("문자열을 쉼표로 파싱한다.") + @Test + void test() { + // given + final String input = "엠제이, 밍트"; + final List expected = List.of("엠제이", "밍트"); + + // when & then + assertThat(StringParser.parseByComma(input)).isEqualTo(expected); + } +}