diff --git a/README.md b/README.md index 556099c4de..89c5e733cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,78 @@ -# java-blackjack +# 연료 주입 -블랙잭 미션 저장소 +## 기능 요구 사항 -## 우아한테크코스 코드리뷰 +1. 렌터카 회사는 이동할 거리가 정해져있는 자동차를 받을 수 있다. +2. 자동차는 Sonata, Avante, K5 3종류가 존재한다. +3. 자동차 회사는 각 차량 별로 주입해야 할 연료량을 확인할 수 있는 보고서를 출력해야 한다. -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +### 자동차 정보 + +자동차 연비는 아래와 같다. + +``` +* Sonata : 10km/리터 +* Avante : 15km/리터 +* K5 : 13km/리터 +``` + +--- + +# 블랙잭 + +## 기능 요구 사항 + +- [x] 게임에 참여할 플레이어의 이름을 입력 받는다. + - 쉼표를 기준으로 분리한다. + - [x] 플레이어명에 포함된 좌우 공백은 제거된다. + - [x] 플레이어명이 입력되지 않으면 예외가 발생한다. + - [x] 플레이어명이 빈 문자열인 경우 예외가 발생한다. + - [x] 플레이어명이 중복된 경우 예외가 발생한다. + +- [x] 딜러와 플레이어의 카드 정보를 출력한다. + - [x] 게임을 시작하면 딜러와 모든 플레이어에게 두 장의 카드가 지급된다. + - 딜러의 경우 두 장 중 한 장만 공개된다. + - 플레이어의 경우 두 장 모두 공개된다. + +- [x] 각 플레이어별로 카드를 더 뽑는다. + - [x] 에이스의 경우 11로 계산하지만, 점수가 21을 초과하는 경우 1로 계산하여 진행 가능 여부를 판단한다. + - [x] 플레이어의 현재 점수가 21을 초과한 경우, 버스트되었다는 메세지를 출력하고 카드 뽑기를 중단한다. + - [x] 플레이어의 현재 점수가 21 이하인 경우 카드를 한 장 더 받을지의 여부를 입력받는다. (예는 y, 아니오는 n) + - [x] y를 입력한 경우 카드 덱에서 한 장의 카드를 뽑아서 지급한다. + - [x] n을 입력한 경우 카드 뽑기를 중단한다. + - [x] y, n 이외의 값을 입력한 경우 예외가 발생한다. + +- [x] 딜러가 한 장의 카드를 더 받을지의 여부는 자동으로 결정된다. + - [x] 에이스의 경우 무조건 11로 계산한다. + - 딜러의 점수가 16이하면 반드시 1장의 카드를 추가로 받는다. + - 딜러의 점수가 17이상이면 추가로 받지 않는다. + +- [x] 딜러와 각 플레이어의 모든 패와 점수가 동시에 공개된다. + - [x] 에이스의 경우 11로 계산하지만, 점수가 21을 초과하는 경우 1로 계산한다. + +- [x] 게임의 결과를 출력한다. + - [x] 각 플레이어별로 딜러와 패를 비교하여 승리, 패배, 무승부 여부를 판단한다. + - [x] 딜러의 패가 21을 초과한 경우, 자동으로 모든 플레이어가 승리한다. + - [x] 딜러의 패가 21이하일 때, 플레이어의 패가 21을 초과한 경우, 플레이어는 패배한다. + - [x] 딜러와 플레이어의 패가 모두 21이하일 때 점수가 더 큰 쪽이 승리한다. + - [x] 딜러의 경우, 모든 플레이어와의 전적을 출력한다. + - [x] 플레이어의 경우, 딜러와의 승부 결과를 출력한다. + +### 카드 정보 + +카드 덱은 다음과 같이 구성된다. + +``` +* 카드는 4가지 심볼로 구성된다. (SPADE, HEART, DIAMOND, CLOVER) +* 카드는 13가지 랭크로 구성된다. (Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King) +* 이에 따라 한 팩의 카드는 52장으로 이루어진다. +* 게임이 시작될 때 52장의 카드는 셔플되며, 맨 위의 카드부터 한 장씩 뽑힌다. +``` + +카드 랭크별 숫자 정보는 아래와 같다. + +``` +* Ace : 1 혹은 11 +* 2~10 : 대응되는 번호를 그대로 사용 +* King, Queen, Jack : 10 +``` diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 0000000000..4f16c05a29 --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,19 @@ +package blackjack; + +import blackjack.controller.BlackjackController; +import blackjack.domain.game.BlackjackGame; + +public class Application { + + public static void main(String[] args) { + BlackjackController blackjackController = new BlackjackController(); + BlackjackGame blackjackGame = blackjackController.initializeGame(); + + blackjackController.printInitialHand(blackjackGame); + blackjackController.giveCardsToAllPlayer(blackjackGame); + blackjackController.giveExtraCardToDealer(blackjackGame); + blackjackController.printFinalHandAndScore(blackjackGame); + blackjackController.printDealerMatchDto(blackjackGame); + blackjackController.printPlayersMatchDto(blackjackGame); + } +} diff --git a/src/main/java/blackjack/controller/BlackjackController.java b/src/main/java/blackjack/controller/BlackjackController.java new file mode 100644 index 0000000000..7a67030f07 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,118 @@ +package blackjack.controller; + + +import blackjack.domain.card.CardDeck; +import blackjack.domain.game.BlackjackGame; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import blackjack.dto.DealerMatchDto; +import blackjack.dto.ParticipantDto; +import blackjack.dto.PlayerMatchDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BlackjackController { + public BlackjackGame initializeGame() { + try { + List playerNames = InputView.requestPlayerNamesInput(); + return new BlackjackGame(new CardDeck(), playerNames); + } catch (IllegalArgumentException exception) { + OutputView.printException(exception); + return initializeGame(); + } + } + + // TODO: 추후 participants 에 dealer 가 포함되는 구조로 개선되면 수정 필요 + public void printInitialHand(BlackjackGame game) { + ParticipantDto dealerDto = ParticipantDto.from(game.getDealer()); + List playerDtos = game.getPlayers() + .stream() + .map(ParticipantDto::from) + .collect(Collectors.toList()); + + printInitialInfo(playerDtos, dealerDto); + } + + private void printInitialInfo(List playerDtos, ParticipantDto dealerDto) { + OutputView.printInitialDistributionInfo(playerDtos); + OutputView.printInitialDealerHand(dealerDto); + OutputView.printInitialPlayersHand(playerDtos); + } + + public void giveCardsToAllPlayer(BlackjackGame game) { + List players = game.getPlayers(); + for (Player player : players) { + giveCardsToPlayer(player, game); + } + } + + private void giveCardsToPlayer(Player player, BlackjackGame game) { + try { + giveSingleCardToPlayerOnMoreCardInput(player, game); + } catch (IllegalArgumentException exception) { + OutputView.printException(exception); + giveCardsToPlayer(player, game); + } + } + + private void giveSingleCardToPlayerOnMoreCardInput(Player player, BlackjackGame game) { + if (InputView.requestMorePlayerCardInput(player.getName())) { + giveSingleCardToPlayer(player, game); + } + } + + private void giveSingleCardToPlayer(Player player, BlackjackGame game) { + if (game.giveExtraCardToPlayer(player)) { + OutputView.printParticipantHand(ParticipantDto.from(player)); + giveCardsToPlayer(player, game); + return; + } + + printHandAndBustMessage(player); + } + + private void printHandAndBustMessage(Player player) { + OutputView.printParticipantHand(ParticipantDto.from(player)); + OutputView.printPlayerBustInfo(); + } + + public void giveExtraCardToDealer(BlackjackGame game) { + int extraCardCount = game.giveExtraCardsToDealer(); + + if (extraCardCount > 0) { + OutputView.printDealerExtraCardInfo(extraCardCount); + } + } + + // TODO: 추후 participants 에 dealer 가 포함되는 구조로 개선되면 수정 필요 + public void printFinalHandAndScore(BlackjackGame game) { + List participants = new ArrayList<>(List.of(ParticipantDto.from(game.getDealer()))); + participants.addAll(game.getPlayers() + .stream() + .map(ParticipantDto::from) + .collect(Collectors.toList())); + + OutputView.printHandAndScore(participants); + } + + public void printDealerMatchDto(BlackjackGame game) { + DealerMatchDto dealerMatchDto = DealerMatchDto.of(game.getDealer(), game.getPlayers()); + OutputView.printDealerMatchResult(dealerMatchDto); + } + + public void printPlayersMatchDto(BlackjackGame game) { + Dealer dealer = game.getDealer(); + + List playerMatchDtos = game.getPlayers() + .stream() + .map(player -> PlayerMatchDto.of(player, dealer)) + .collect(Collectors.toList()); + + OutputView.printPlayerMatchResults(playerMatchDtos); + } + +} + 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 0000000000..db215cd7db --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,55 @@ +package blackjack.domain.card; + +import blackjack.domain.game.Score; +import java.util.HashSet; +import java.util.Set; + +public class Card { + private final CardRank rank; + private final CardSymbol symbol; + + private Card(CardRank rank, CardSymbol symbol) { + this.rank = rank; + this.symbol = symbol; + } + + public static Card of(CardRank rank, CardSymbol symbol) { + return CardCache.getCache(rank, symbol); + } + + public boolean isAce() { + return rank == CardRank.ACE; + } + + public Score getRankValue() { + return rank.getValue(); + } + + public String getName() { + return rank.getDisplayName() + symbol.getDisplayName(); + } + + @Override + public String toString() { + return "Card{" + getName() + "}"; + } + + private static class CardCache { + static Set cache = new HashSet<>(); + + static Card getCache(CardRank rank, CardSymbol symbol) { + return cache.stream() + .filter(card -> card.rank == rank) + .filter(card -> card.symbol == symbol) + .findAny() + .orElseGet(() -> createNewCache(rank, symbol)); + } + + static Card createNewCache(CardRank rank, CardSymbol symbol) { + Card newCard = new Card(rank, symbol); + cache.add(newCard); + + return newCard; + } + } +} diff --git a/src/main/java/blackjack/domain/card/CardDeck.java b/src/main/java/blackjack/domain/card/CardDeck.java new file mode 100644 index 0000000000..fceea57c22 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardDeck.java @@ -0,0 +1,34 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; + +public class CardDeck implements CardStack { + private final LinkedList cards = new LinkedList<>(); + + public CardDeck() { + initCards(); + Collections.shuffle(cards); + } + + private void initCards() { + Arrays.stream(CardRank.values()) + .forEach(this::createAllSymbols); + } + + private void createAllSymbols(CardRank rank) { + Arrays.stream(CardSymbol.values()) + .forEach((symbol -> cards.add(Card.of(rank, symbol)))); + } + + public Card pop() { + // TODO: handle NoSuchElementException on popping on empty list + return cards.pop(); + } + + @Override + public String toString() { + return "CardDeck{" + "cards=" + cards + '}'; + } +} diff --git a/src/main/java/blackjack/domain/card/CardRank.java b/src/main/java/blackjack/domain/card/CardRank.java new file mode 100644 index 0000000000..56e8f5a4c5 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardRank.java @@ -0,0 +1,36 @@ +package blackjack.domain.card; + +import blackjack.domain.game.Score; + +public enum CardRank { + + ACE("A", Score.valueOf(11)), + TWO("2", Score.valueOf(2)), + THREE("3", Score.valueOf(3)), + FOUR("4", Score.valueOf(4)), + FIVE("5", Score.valueOf(5)), + SIX("6", Score.valueOf(6)), + SEVEN("7", Score.valueOf(7)), + EIGHT("8", Score.valueOf(8)), + NINE("9", Score.valueOf(9)), + TEN("10", Score.valueOf(10)), + JACK("J", Score.valueOf(10)), + QUEEN("Q", Score.valueOf(10)), + KING("K", Score.valueOf(10)); + + private final String displayName; + private final Score value; + + CardRank(final String displayName, final Score value) { + this.displayName = displayName; + this.value = value; + } + + public String getDisplayName() { + return displayName; + } + + public Score getValue() { + return value; + } +} diff --git a/src/main/java/blackjack/domain/card/CardStack.java b/src/main/java/blackjack/domain/card/CardStack.java new file mode 100644 index 0000000000..f3e1c5e76d --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardStack.java @@ -0,0 +1,5 @@ +package blackjack.domain.card; + +public interface CardStack { + Card pop(); +} diff --git a/src/main/java/blackjack/domain/card/CardSymbol.java b/src/main/java/blackjack/domain/card/CardSymbol.java new file mode 100644 index 0000000000..ba47830905 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardSymbol.java @@ -0,0 +1,18 @@ +package blackjack.domain.card; + +public enum CardSymbol { + HEART("하트"), + SPADE("스페이드"), + DIAMOND("다이아몬드"), + CLOVER("클로버"); + + private final String displayName; + + CardSymbol(final String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} 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 0000000000..b743afa90e --- /dev/null +++ b/src/main/java/blackjack/domain/card/Hand.java @@ -0,0 +1,69 @@ +package blackjack.domain.card; + +import blackjack.domain.game.Score; +import java.util.HashSet; +import java.util.Set; + +public class Hand { + private static final String NO_DUPLICATE_CARD_EXCEPTION_MESSAGE = "중복된 카드는 존재할 수 없습니다."; + private static final int VALUE_FOR_ADJUST_ACE_VALUE_TO_SMALL = 10; + + private final Set cards; + + private Hand(Set cards) { + this.cards = cards; + } + + public static Hand of(Card card1, Card card2) { + Set initialCards = new HashSet<>(Set.of(card1, card2)); + return new Hand(initialCards); + } + + public void add(Card card) { + validateNoDuplicate(card); + cards.add(card); + } + + private void validateNoDuplicate(Card card) { + if (cards.contains(card)) { + throw new IllegalArgumentException(NO_DUPLICATE_CARD_EXCEPTION_MESSAGE); + } + } + + public Set getCards() { + return Set.copyOf(cards); + } + + public Score getScore() { + int maximumScore = cards.stream() + .map(Card::getRankValue) + .reduce(Score.valueOf(0), Score::add) + .getValue(); + + int aceCount = (int) cards.stream() + .filter(Card::isAce) + .count(); + + return calculateScoreIncludingAce(maximumScore, aceCount); + } + + private Score calculateScoreIncludingAce(int maximumScore, int aceCount) { + int adjustedScore = maximumScore; + + for (int i = 0; i < aceCount; i++) { + if (adjustedScore <= Score.BLACKJACK) { + break; // TODO: 2 depth 수정하기 + } + adjustedScore -= VALUE_FOR_ADJUST_ACE_VALUE_TO_SMALL; + } + + return Score.valueOf(adjustedScore); + } + + @Override + public String toString() { + return "Hand{" + + "cards=" + cards + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/game/BlackjackGame.java b/src/main/java/blackjack/domain/game/BlackjackGame.java new file mode 100644 index 0000000000..82c9ee27a6 --- /dev/null +++ b/src/main/java/blackjack/domain/game/BlackjackGame.java @@ -0,0 +1,92 @@ +package blackjack.domain.game; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardStack; +import blackjack.domain.card.Hand; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BlackjackGame { + + private static final String NO_PLAYER_EXCEPTION_MESSAGE = "플레이어가 없는 게임은 존재할 수 없습니다."; + + // TODO: 필드 3개를 2개로 줄여야함. ParticipantGroup 도메인을 만들어 해결할 예정. + private final CardStack cardDeck; + private final Dealer dealer; + private final List players = new ArrayList<>(); + + public BlackjackGame(CardStack cardDeck, List playerNames) { + validatePlayerNamesNotEmpty(playerNames); + validatePlayerNamesNotDuplicate(playerNames); + + this.cardDeck = cardDeck; + this.dealer = Dealer.of(generateInitialHand()); + players.addAll(initializePlayers(playerNames)); + } + + private List initializePlayers(List playerNames) { + return playerNames.stream() + .map(name -> Player.of(name, generateInitialHand())) + .collect(Collectors.toList()); + } + + private void validatePlayerNamesNotDuplicate(List playerNames) { + int originalSize = playerNames.size(); + int distinctSize = (int) playerNames.stream() + .distinct() + .count(); + + if (originalSize != distinctSize) { + throw new IllegalArgumentException("플레이어의 이름은 중복될 수 없습니다."); + } + } + + private void validatePlayerNamesNotEmpty(List playerNames) { + if (playerNames.isEmpty()) { + throw new IllegalArgumentException(NO_PLAYER_EXCEPTION_MESSAGE); + } + } + + private Hand generateInitialHand() { + return Hand.of(cardDeck.pop(), cardDeck.pop()); + } + + public boolean giveExtraCardToPlayer(Player player) { + player.receiveCard(cardDeck.pop()); + return player.canReceive(); + } + + public int giveExtraCardsToDealer() { + int extraCardCount = 0; + while (dealer.canReceive()) { + dealer.receiveCard(cardDeck.pop()); + extraCardCount++; + } + + return extraCardCount; + } + + public Card popCard() { + return cardDeck.pop(); + } + + public Dealer getDealer() { + return dealer; + } + + public List getPlayers() { + return List.copyOf(players); + } + + @Override + public String toString() { + return "BlackjackGame{" + + "cardDeck=" + cardDeck + + ", dealer=" + dealer + + ", players=" + players + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/game/Referee.java b/src/main/java/blackjack/domain/game/Referee.java new file mode 100644 index 0000000000..89b02c4936 --- /dev/null +++ b/src/main/java/blackjack/domain/game/Referee.java @@ -0,0 +1,36 @@ +package blackjack.domain.game; + +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Participant; +import blackjack.domain.participant.Player; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; + +public class Referee { + + public Map generateDealerResult(Dealer dealer, Collection players) { + Map results = generateDefaultResults(); + + for (Player player : players) { + ResultType result = dealer.compareWith(player); + results.get(result).increment(); + } + + return results; + } + + private Map generateDefaultResults() { + Map results = new EnumMap<>(ResultType.class); + + for (ResultType type : ResultType.values()) { + results.put(type, new ResultCount(0)); + } + + return results; + } + + public ResultType generatePlayerResult(Player player, Participant other) { + return player.compareWith(other); + } +} \ No newline at end of file diff --git a/src/main/java/blackjack/domain/game/ResultCount.java b/src/main/java/blackjack/domain/game/ResultCount.java new file mode 100644 index 0000000000..7a59fdeb29 --- /dev/null +++ b/src/main/java/blackjack/domain/game/ResultCount.java @@ -0,0 +1,47 @@ +package blackjack.domain.game; + +import java.util.Objects; + +public class ResultCount { + + private int value; + + // TODO: 불변객체로 변경 + public ResultCount() { + value = 0; + } + + public ResultCount(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public void increment() { + value++; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResultCount that = (ResultCount) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "ResultCount{" + "value=" + value + '}'; + } +} diff --git a/src/main/java/blackjack/domain/game/ResultType.java b/src/main/java/blackjack/domain/game/ResultType.java new file mode 100644 index 0000000000..09f6a8ceb0 --- /dev/null +++ b/src/main/java/blackjack/domain/game/ResultType.java @@ -0,0 +1,17 @@ +package blackjack.domain.game; + +public enum ResultType { + WIN("승"), + LOSE("패"), + DRAW("무"); + + private final String displayName; + + ResultType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/blackjack/domain/game/Score.java b/src/main/java/blackjack/domain/game/Score.java new file mode 100644 index 0000000000..ebb7cdc7d6 --- /dev/null +++ b/src/main/java/blackjack/domain/game/Score.java @@ -0,0 +1,62 @@ +package blackjack.domain.game; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class Score implements Comparable { + public static final int DEALER_EXTRA_CARD_LIMIT = 16; + public static final int BLACKJACK = 21; + + private final int value; + + // TODO: 음수 값에 대한 유효성 검증 + private Score(final int value) { + this.value = value; + } + + public static Score valueOf(final int value) { + return ScoreCache.getCache(value); + } + + public Score add(Score anotherScore) { + int addedValue = this.value + anotherScore.value; + return ScoreCache.getCache(addedValue); + } + + public boolean isGreaterThan(int score) { + return this.value > score; + } + + public boolean isLessOrEqualThan(int score) { + return this.value <= score; + } + + public int getValue() { + return value; + } + + @Override + public int compareTo(Score o) { + return Integer.compare(this.value, o.value); + } + + @Override + public String toString() { + return "Score{" + "value=" + value + '}'; + } + + private static class ScoreCache { + static Map cache = new HashMap<>(); + + static Score getCache(final int value) { + return Optional.ofNullable(cache.get(value)) + .orElseGet(() -> createNewCache(value)); + } + + static Score createNewCache(final int value) { + cache.put(value, new Score(value)); + return cache.get(value); + } + } +} diff --git a/src/main/java/blackjack/domain/participant/Dealer.java b/src/main/java/blackjack/domain/participant/Dealer.java new file mode 100644 index 0000000000..f564e52b2f --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,35 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.game.Score; + +public class Dealer extends Participant { + + private static final String DEALER_NAME = "딜러"; + + private Dealer(final String name, final Hand hand) { + super(name, hand); + } + + public static Dealer of(final Hand hand) { + return new Dealer(DEALER_NAME, hand); + } + + public void receiveCard(Card card) { + getHand().add(card); + } + + public boolean canReceive() { + Score score = getHand().getScore(); + return score.isLessOrEqualThan(Score.DEALER_EXTRA_CARD_LIMIT); + } + + @Override + public String toString() { + return "Dealer{" + + "name='" + getName() + '\'' + + ", hand=" + getHand() + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/participant/Participant.java b/src/main/java/blackjack/domain/participant/Participant.java new file mode 100644 index 0000000000..e18d6aa571 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participant.java @@ -0,0 +1,52 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.game.ResultType; +import blackjack.domain.game.Score; + +public abstract class Participant { + + private final String name; + private final Hand hand; + + protected Participant(final String name, final Hand hand) { + this.name = name; + this.hand = hand; + } + + public abstract void receiveCard(Card card); + + public abstract boolean canReceive(); + + public Score getCurrentScore() { + return hand.getScore(); + } + + public String getName() { + return name; + } + + public Hand getHand() { + return hand; + } + + // TODO: 구현체의 canReceive 메소드와 상당히 겹침. 해결 필요. + public boolean isBusted() { + return hand.getScore().isGreaterThan(Score.BLACKJACK); + } + + // TODO: 조건식 단순화 + public ResultType compareWith(Participant other) { + int playerScore = getCurrentScore().getValue(); + int otherScore = other.getCurrentScore().getValue(); + + if ((playerScore > otherScore && !isBusted()) || (!isBusted() && other.isBusted())) { + return ResultType.WIN; + } + if ((playerScore < otherScore && !other.isBusted()) || (isBusted() && !other.isBusted())) { + return ResultType.LOSE; + } + return ResultType.DRAW; + } +} 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 0000000000..ba1fdede3a --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,42 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.game.Score; + +public class Player extends Participant { + + private static final String INVALID_NAME_LENGTH_EXCEPTION_MESSAGE = "이름은 1글자 이상이어야합니다."; + + private Player(final String name, final Hand hand) { + super(name, hand); + } + + public static Player of(final String name, final Hand hand) { + validateNameNotEmpty(name); + return new Player(name, hand); + } + + private static void validateNameNotEmpty(String name) { + if (name.isEmpty()) { + throw new IllegalArgumentException(INVALID_NAME_LENGTH_EXCEPTION_MESSAGE); + } + } + + public void receiveCard(Card card) { + getHand().add(card); + } + + public boolean canReceive() { + Score score = getHand().getScore(); + return score.isLessOrEqualThan(Score.BLACKJACK); + } + + @Override + public String toString() { + return "Player{" + + "name='" + getName() + '\'' + + ", hand=" + getHand() + + '}'; + } +} diff --git a/src/main/java/blackjack/dto/DealerMatchDto.java b/src/main/java/blackjack/dto/DealerMatchDto.java new file mode 100644 index 0000000000..bf6c8dcc96 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerMatchDto.java @@ -0,0 +1,42 @@ +package blackjack.dto; + +import blackjack.domain.game.Referee; +import blackjack.domain.game.ResultCount; +import blackjack.domain.game.ResultType; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class DealerMatchDto { + private static final Referee REFEREE = new Referee(); + + private final Map matchResult; + private final String name; + + private DealerMatchDto(Map matchResult, String name) { + this.matchResult = matchResult; + this.name = name; + } + + public static DealerMatchDto of(Dealer dealer, List players) { + return new DealerMatchDto(REFEREE.generateDealerResult(dealer, players), dealer.getName()); + } + + public String getName() { + return name; + } + + public Map getMatchResult() { + return new EnumMap<>(matchResult); + } + + @Override + public String toString() { + return "DealerMatchDto{" + + "matchResult=" + matchResult + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/blackjack/dto/HandDto.java b/src/main/java/blackjack/dto/HandDto.java new file mode 100644 index 0000000000..8bf4310fcb --- /dev/null +++ b/src/main/java/blackjack/dto/HandDto.java @@ -0,0 +1,51 @@ +package blackjack.dto; + +import blackjack.domain.card.Card; +import blackjack.domain.participant.Participant; +import java.util.List; +import java.util.stream.Collectors; + +public class HandDto { + private final List cards; + private final int score; + + private HandDto(List cards, int score) { + this.cards = cards; + this.score = score; + } + + public static HandDto of(Participant participant) { + List cards = getCards(participant); + int score = participant.getCurrentScore().getValue(); + + return new HandDto(cards, score); + } + + private static List getCards(Participant participant) { + return participant.getHand() + .getCards() + .stream() + .map(Card::getName) + .collect(Collectors.toList()); + } + + public List getCards() { + return List.copyOf(cards); + } + + public String getFirstCard() { + return cards.get(0); + } + + public int getScore() { + return score; + } + + @Override + public String toString() { + return "HandDto{" + + "cards=" + cards + + ", score=" + score + + '}'; + } +} diff --git a/src/main/java/blackjack/dto/ParticipantDto.java b/src/main/java/blackjack/dto/ParticipantDto.java new file mode 100644 index 0000000000..afa603e28c --- /dev/null +++ b/src/main/java/blackjack/dto/ParticipantDto.java @@ -0,0 +1,33 @@ +package blackjack.dto; + +import blackjack.domain.participant.Participant; + +public class ParticipantDto { + private final String name; + private final HandDto handDto; + + private ParticipantDto(String name, HandDto handDto) { + this.name = name; + this.handDto = handDto; + } + + public static ParticipantDto from(Participant participant) { + return new ParticipantDto(participant.getName(), HandDto.of(participant)); + } + + public String getName() { + return name; + } + + public HandDto getHandDto() { + return handDto; + } + + @Override + public String toString() { + return "ParticipantDto{" + + "name='" + name + '\'' + + ", handDto=" + handDto + + '}'; + } +} diff --git a/src/main/java/blackjack/dto/PlayerMatchDto.java b/src/main/java/blackjack/dto/PlayerMatchDto.java new file mode 100644 index 0000000000..0b52f1525a --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerMatchDto.java @@ -0,0 +1,38 @@ +package blackjack.dto; + +import blackjack.domain.game.Referee; +import blackjack.domain.game.ResultType; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; + +public class PlayerMatchDto { + private static final Referee REFEREE = new Referee(); + + private final ResultType matchResult; + private final String name; + + private PlayerMatchDto(ResultType matchResult, String name) { + this.matchResult = matchResult; + this.name = name; + } + + public static PlayerMatchDto of(Player player, Dealer dealer) { + return new PlayerMatchDto(REFEREE.generatePlayerResult(player, dealer), player.getName()); + } + + public String getName() { + return name; + } + + public ResultType getMatchResult() { + return matchResult; + } + + @Override + public String toString() { + return "PlayerMatchDto{" + + "matchResult=" + matchResult + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..fd8dda6778 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,50 @@ +package blackjack.view; + +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class InputView { + + private static final String REQUEST_PLAYER_NAMES_INPUT_MESSAGE = "게임에 참여할 사람의 이름을 입력하세요. (쉼표 기준으로 분리)"; + private static final String REQUEST_MORE_PLAYER_CARD_INPUT_TEXT = "는 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)"; + private static final String INVALID_MORE_PLAYER_CARD_INPUT_EXCEPTION_MESSAGE = "y 혹은 n만 입력이 가능합니다."; + + private static final String NAME_INPUT_DELIMITER = ","; + private static final String YES_TEXT = "y"; + private static final String NO_TEXT = "n"; + + private static final Scanner scanner = new Scanner(System.in); + + public static List requestPlayerNamesInput() { + print(REQUEST_PLAYER_NAMES_INPUT_MESSAGE); + String input = scanner.nextLine(); + + return getTrimmedStringListOf(input); + } + + private static List getTrimmedStringListOf(String input) { + return Stream.of(input.split(NAME_INPUT_DELIMITER)) + .map(String::trim) + .collect(Collectors.toList()); + } + + private static void print(String text) { + System.out.println(text); + } + + public static boolean requestMorePlayerCardInput(String playerName) { + print(playerName + REQUEST_MORE_PLAYER_CARD_INPUT_TEXT); + String input = scanner.nextLine(); + + validateInvalidMoreCardInput(input); + return input.equals(YES_TEXT); + } + + private static void validateInvalidMoreCardInput(String input) { + if (!input.equals(YES_TEXT) && !input.equals(NO_TEXT)) { + throw new IllegalArgumentException(INVALID_MORE_PLAYER_CARD_INPUT_EXCEPTION_MESSAGE); + } + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 0000000000..55420fd172 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,112 @@ +package blackjack.view; + +import blackjack.domain.game.ResultCount; +import blackjack.domain.game.ResultType; +import blackjack.dto.DealerMatchDto; +import blackjack.dto.ParticipantDto; +import blackjack.dto.PlayerMatchDto; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String NEW_LINE = System.lineSeparator(); + + private static final String JOIN_DELIMITER = ", "; + private static final String COLON_SEPARATOR = ": "; + private static final String WHITE_SPACE = " "; + + private static final String INITIAL_CARD_DISTRIBUTION_MESSAGE = NEW_LINE + "딜러와 %s에게 2장의 카드를 나누었습니다." + NEW_LINE; + private static final String NAME_AND_HAND_FORMAT = "%s 카드: %s" + NEW_LINE; + private static final String PLAYER_BUST_MESSAGE = "버스트! 21을 초과하였습니다!"; + private static final String DEALER_EXTRA_CARD_FORMAT = NEW_LINE + "딜러는 16이하라 %d장의 카드를 더 받았습니다." + NEW_LINE; + private static final String FINAL_RESULT_MESSAGE = NEW_LINE + "## 최종 승패"; + private static final String PLAYER_MATCH_RESULT_FORMAT = "%s: %s" + NEW_LINE; + private static final String PARTICIPANT_HAND_AND_SCORE_FORMAT = "%s 카드: %s - 결과: %d" + NEW_LINE; + private static final String EXCEPTION_MESSAGE_FORMAT = "[ERROR] %s" + NEW_LINE; + + public static void printInitialDistributionInfo(List playerDtos) { + String names = playerDtos.stream() + .map(ParticipantDto::getName) + .collect(Collectors.joining(JOIN_DELIMITER)); + + System.out.printf(INITIAL_CARD_DISTRIBUTION_MESSAGE, names); + } + + public static void printInitialDealerHand(ParticipantDto dealerDto) { + System.out.printf(NAME_AND_HAND_FORMAT, dealerDto.getName(), dealerDto.getHandDto().getFirstCard()); + } + + public static void printInitialPlayersHand(List playerDtos) { + playerDtos.forEach(OutputView::printParticipantHand); + printNewLine(); + } + + public static void printParticipantHand(ParticipantDto dto) { + String name = dto.getName(); + String cards = String.join(JOIN_DELIMITER, dto.getHandDto().getCards()); + + System.out.printf(NAME_AND_HAND_FORMAT, name, cards); + } + + public static void printPlayerBustInfo() { + System.out.println(PLAYER_BUST_MESSAGE); + } + + public static void printDealerExtraCardInfo(int count) { + System.out.printf(DEALER_EXTRA_CARD_FORMAT, count); + } + + public static void printHandAndScore(List participantDtos) { + participantDtos.forEach(OutputView::printSingleHandAndScore); + } + + private static void printSingleHandAndScore(ParticipantDto dto) { + String name = dto.getName(); + String cards = String.join(JOIN_DELIMITER, dto.getHandDto().getCards()); + int score = dto.getHandDto().getScore(); + + System.out.printf(PARTICIPANT_HAND_AND_SCORE_FORMAT, name, cards, score); + } + + public static void printDealerMatchResult(DealerMatchDto dealerMatchDto) { + System.out.println(FINAL_RESULT_MESSAGE); + System.out.print(dealerMatchDto.getName() + COLON_SEPARATOR); + + dealerMatchDto.getMatchResult() + .entrySet() + .forEach(OutputView::printSingleDealerMatchResult); + + printNewLine(); + } + + private static void printSingleDealerMatchResult(Entry entrySet) { + int resultCount = entrySet.getValue().getValue(); + String resultTypeName = entrySet.getKey().getDisplayName(); + + if (resultCount > 0) { + System.out.print(resultCount + resultTypeName + WHITE_SPACE); + } + } + + public static void printPlayerMatchResults(Collection playerMatchDtos) { + playerMatchDtos.forEach(OutputView::printSinglePlayerMatchResult); + } + + public static void printException(RuntimeException exception) { + System.out.printf(EXCEPTION_MESSAGE_FORMAT, exception.getMessage()); + } + + private static void printSinglePlayerMatchResult(PlayerMatchDto playerMatchDto) { + String name = playerMatchDto.getName(); + String matchResult = playerMatchDto.getMatchResult().getDisplayName(); + + System.out.printf(PLAYER_MATCH_RESULT_FORMAT, name, matchResult); + } + + private static void printNewLine() { + System.out.println(); + } +} diff --git a/src/main/java/fuel/Avante.java b/src/main/java/fuel/Avante.java new file mode 100644 index 0000000000..3780e7a9ef --- /dev/null +++ b/src/main/java/fuel/Avante.java @@ -0,0 +1,19 @@ +package fuel; + +public class Avante implements Car { + private final String name = "Avante"; + private final int fuelEfficiency = 15; + private final int distance; + + public Avante(int distance) { + this.distance = distance; + } + + public String getName() { + return name; + } + + public int getFuelNeeded() { + return distance / fuelEfficiency; + } +} diff --git a/src/main/java/fuel/Car.java b/src/main/java/fuel/Car.java new file mode 100644 index 0000000000..4a24112f65 --- /dev/null +++ b/src/main/java/fuel/Car.java @@ -0,0 +1,7 @@ +package fuel; + +public interface Car { + String getName(); + + int getFuelNeeded(); +} diff --git a/src/main/java/fuel/K5.java b/src/main/java/fuel/K5.java new file mode 100644 index 0000000000..3929058cea --- /dev/null +++ b/src/main/java/fuel/K5.java @@ -0,0 +1,19 @@ +package fuel; + +public class K5 implements Car { + private final String name = "K5"; + private final int fuelEfficiency = 13; + private final int distance; + + public K5(int distance) { + this.distance = distance; + } + + public String getName() { + return name; + } + + public int getFuelNeeded() { + return distance / fuelEfficiency; + } +} diff --git a/src/main/java/fuel/RentCompany.java b/src/main/java/fuel/RentCompany.java new file mode 100644 index 0000000000..8bc346f7c0 --- /dev/null +++ b/src/main/java/fuel/RentCompany.java @@ -0,0 +1,35 @@ +package fuel; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class RentCompany { + private static final String CAR_FUEL_REPORT_FORMAT = "%s : %d리터%n"; + + private final List cars; + + private RentCompany() { + cars = new ArrayList<>(); + } + + public static RentCompany create() { + return new RentCompany(); + } + + public List addCar(Car car) { + cars.add(car); + return cars; + } + + public String generateReport() { + return cars.stream() + .map(this::getCarFuelReport) + .collect(Collectors.joining()); + } + + private String getCarFuelReport(Car car) { + return String.format(CAR_FUEL_REPORT_FORMAT, car.getName(), car.getFuelNeeded()); + } +} + diff --git a/src/main/java/fuel/Sonata.java b/src/main/java/fuel/Sonata.java new file mode 100644 index 0000000000..6122575260 --- /dev/null +++ b/src/main/java/fuel/Sonata.java @@ -0,0 +1,19 @@ +package fuel; + +public class Sonata implements Car { + private final String name = "Sonata"; + private final int fuelEfficiency = 10; + private final int distance; + + public Sonata(int distance) { + this.distance = distance; + } + + public String getName() { + return name; + } + + public int getFuelNeeded() { + return distance / fuelEfficiency; + } +} diff --git a/src/test/java/blackjack/domain/card/CardDeckTest.java b/src/test/java/blackjack/domain/card/CardDeckTest.java new file mode 100644 index 0000000000..a8f4dfd08d --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardDeckTest.java @@ -0,0 +1,51 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CardDeckTest { + private CardDeck cardDeck; + + @BeforeEach + void setUp() { + cardDeck = new CardDeck(); + } + + @DisplayName("CardDeck 인스턴스가 생성된다.") + @Test + void constructor() { + assertThat(cardDeck).isNotNull(); + } + + @DisplayName("pop 메서드는 서로 중복되지 않는 카드를 카드덱에서 뽑아온다.") + @Test + void pop_eachCardIsUnique() { + Card card1 = cardDeck.pop(); + Card card2 = cardDeck.pop(); + Card card3 = cardDeck.pop(); + + int originalSize = List.of(card1, card2, card3).size(); + int noDuplicateSize = Set.of(card1, card2, card3).size(); + + assertThat(originalSize).isEqualTo(noDuplicateSize); + } + + @DisplayName("pop 메서드는 52회까지 실행할 수 있으며, 53번째에서는 예외가 발생한다.") + @Test + void pop_cardDeckConsistsOf52Cards() { + for (int i = 0; i < 52; i++) { + Assertions.assertDoesNotThrow(() -> cardDeck.pop()); + } + + assertThatThrownBy(() -> cardDeck.pop()) + .isInstanceOf(NoSuchElementException.class); + } +} diff --git a/src/test/java/blackjack/domain/card/CardRankTest.java b/src/test/java/blackjack/domain/card/CardRankTest.java new file mode 100644 index 0000000000..f1d98966d7 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardRankTest.java @@ -0,0 +1,69 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.game.Score; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class CardRankTest { + private static final int RANK_LENGTH = 13; + + @DisplayName("CardRank 인스턴스가 13개 생성된다.") + @Test + void init() { + int actual = CardRank.values().length; + + assertThat(actual).isEqualTo(RANK_LENGTH); + } + + @DisplayName("getDisplayName 은 카드 랭크의 이름을 반환한다.") + @Test + void getDisplayName() { + List ranks = List.of(CardRank.values()); + List names = List.of("A", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "J", "Q", "K"); + + for (int i = 0; i < RANK_LENGTH; i++) { + assertThat(ranks.get(i).getDisplayName()) + .isEqualTo(names.get(i)); + } + } + + @DisplayName("ACE 는 기본값으로 11의 Score 를 지닌다.") + @Test + void getValue_Ace() { + Score actual = CardRank.ACE.getValue(); + Score expected = Score.valueOf(11); + + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("TWO ~ TEN 은 각각 대응되는 값의 Score 를 지닌다.") + @ParameterizedTest(name = "[{index}] 랭크: {0}, 값: {1}") + @CsvSource(value = {"TWO,2", "THREE,3", "FOUR,4", "FIVE,5", "SIX,6", "SEVEN,7", "EIGHT,8", "NINE,9", "TEN,10"}) + void getValue_TwoToTen(String rankName, int value) { + CardRank numberCard = CardRank.valueOf(rankName); + + Score actual = numberCard.getValue(); + Score expected = Score.valueOf(value); + + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("JACK, QUEEN, KING 은 값으로 10의 Score 를 지닌다.") + @ParameterizedTest(name = "[{index}] 랭크: {0}, 값: 10") + @ValueSource(strings = {"JACK", "QUEEN", "KING"}) + void getValue_JackQueenKing(String rankName) { + CardRank numberCard = CardRank.valueOf(rankName); + + Score actual = numberCard.getValue(); + Score expected = Score.valueOf(10); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/card/CardSymbolTest.java b/src/test/java/blackjack/domain/card/CardSymbolTest.java new file mode 100644 index 0000000000..27b0b0435c --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardSymbolTest.java @@ -0,0 +1,31 @@ +package blackjack.domain.card; + +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 CardSymbolTest { + private static final int SYMBOL_LENGTH = 4; + + @DisplayName("CardSymbol 인스턴스가 4개 생성된다.") + @Test + void init() { + int actual = CardSymbol.values().length; + + assertThat(actual).isEqualTo(SYMBOL_LENGTH); + } + + @DisplayName("getDisplayName 은 카드 심볼의 이름을 반환한다.") + @Test + void getDisplayName() { + List symbols = List.of(CardSymbol.values()); + List names = List.of("하트", "스페이드", "다이아몬드", "클로버"); + + for (int i = 0; i < SYMBOL_LENGTH; i++) { + assertThat(symbols.get(i).getDisplayName()) + .isEqualTo(names.get(i)); + } + } +} \ No newline at end of file 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 0000000000..1e23c5a93b --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,48 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.game.Score; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CardTest { + @DisplayName("정적 팩토리 메소드 of 로 새로운 카드를 생성한다.") + @Test + void of_createNewCard() { + Card card = Card.of(CardRank.ACE, CardSymbol.CLOVER); + + assertThat(card).isNotNull(); + } + + @DisplayName("정적 팩토리 메소드 of 는 캐싱된 카드를 가져온다.") + @Test + void of_getCache() { + Card card = Card.of(CardRank.ACE, CardSymbol.CLOVER); + Card sameCard = Card.of(CardRank.ACE, CardSymbol.CLOVER); + + assertThat(card).isEqualTo(sameCard); + } + + @DisplayName("getRankValue 는 카드 랭크에 담긴 값을 가져온다.") + @Test + void getRankValue() { + Card card = Card.of(CardRank.FIVE, CardSymbol.CLOVER); + + Score actual = card.getRankValue(); + Score expected = Score.valueOf(5); + + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("getName 은 카드의 이름을 반환한다.") + @Test + void getName() { + Card card = Card.of(CardRank.FIVE, CardSymbol.CLOVER); + + String actual = card.getName(); + String expected = "5클로버"; + + assertThat(actual).isEqualTo(expected); + } +} 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 0000000000..f5c379519a --- /dev/null +++ b/src/test/java/blackjack/domain/card/HandTest.java @@ -0,0 +1,141 @@ +package blackjack.domain.card; + +import static blackjack.domain.fixture.CardRepository.CLOVER2; +import static blackjack.domain.fixture.CardRepository.CLOVER4; +import static blackjack.domain.fixture.CardRepository.CLOVER5; +import static blackjack.domain.fixture.CardRepository.CLOVER6; +import static blackjack.domain.fixture.CardRepository.CLOVER9; +import static blackjack.domain.fixture.CardRepository.CLOVER_ACE; +import static blackjack.domain.fixture.CardRepository.CLOVER_KING; +import static blackjack.domain.fixture.CardRepository.CLOVER_QUEEN; +import static blackjack.domain.fixture.CardRepository.HEART_ACE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import blackjack.domain.game.Score; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class HandTest { + + @DisplayName("of 팩토리 메소드는 두개의 카드를 받아 Hand 인스턴스를 생성한다.") + @Test + void of_initsNewHandWithTwoCards() { + CardDeck cardDeck = new CardDeck(); + Hand hand = Hand.of(cardDeck.pop(), cardDeck.pop()); + + assertThat(hand).isNotNull(); + } + + @DisplayName("add 메서드로 새로운 카드를 추가할 수 있다.") + @Test + void add() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + hand.add(CLOVER6); + + assertThat(hand.getCards()) + .contains(CLOVER4, CLOVER5, CLOVER6); + } + + @DisplayName("add 메서드로 중복된 카드를 추가하려고 하면 예외가 발생한다.") + @Test + void add_throwsExceptionOnDuplicateCard() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> hand.add(CLOVER5)) + .withMessage("중복된 카드는 존재할 수 없습니다."); + } + + @DisplayName("getScore 는 각 카드가 지닌 값들의 합을 합산하여 반환한다.") + @Test + void getScore() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + + Score actual = hand.getScore(); + Score expected = Score.valueOf(9); + + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("getScore 는 패에 ACE 가 포함되었을 때 최대한 버스트되지 않고 최대값이 되도록 ACE 값을 선택해야한다.") + @Nested + class getScore_withAce { + + @DisplayName("패가 ACE, 2 일경우 ACE 는 11로 취급된다.") + @Test + void getScore_withAceAnd2() { + // given + Hand hand = Hand.of(CLOVER_ACE, CLOVER2); + + // when + Score actual = hand.getScore(); + Score expected = Score.valueOf(13); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("패가 ACE, ACE 일경우 첫번째 ACE는 11로, 두번째 ACE는 1로 취급된다.") + @Test + void getScore_withTwoAces() { + // given + Hand hand = Hand.of(CLOVER_ACE, HEART_ACE); + + // when + Score actual = hand.getScore(); + Score expected = Score.valueOf(12); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("패가 KING, QUEEN, ACE 일경우 ACE는 1로 취급된다.") + @Test + void getScore_withKingQueenAndAce() { + // given + Hand hand = Hand.of(CLOVER_KING, CLOVER_QUEEN); + hand.add(CLOVER_ACE); + + // when + Score actual = hand.getScore(); + Score expected = Score.valueOf(21); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("패가 KING, 9, ACE, ACE 일경우 두 ACE는 모두 1로 취급된다.") + @Test + void getScore_withKingAceAnd9() { + // given + Hand hand = Hand.of(CLOVER_KING, CLOVER9); + hand.add(CLOVER_ACE); + hand.add(HEART_ACE); + + // when + Score actual = hand.getScore(); + Score expected = Score.valueOf(21); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("패가 KING, QUEEN, ACE, ACE 일경우 두 ACE는 모두 1로 취급된다.") + @Test + void getScore_withKingQueenAndTwoAces() { + // given + Hand hand = Hand.of(CLOVER_KING, CLOVER_QUEEN); + hand.add(CLOVER_ACE); + hand.add(HEART_ACE); + + // when + Score actual = hand.getScore(); + Score expected = Score.valueOf(22); + + // then + assertThat(actual).isEqualTo(expected); + } + } +} diff --git a/src/test/java/blackjack/domain/fixture/CardRepository.java b/src/test/java/blackjack/domain/fixture/CardRepository.java new file mode 100644 index 0000000000..a1620fbc0b --- /dev/null +++ b/src/test/java/blackjack/domain/fixture/CardRepository.java @@ -0,0 +1,22 @@ +package blackjack.domain.fixture; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardRank; +import blackjack.domain.card.CardSymbol; + +// TODO: 클래스명 변경 +public class CardRepository { + public static final Card CLOVER_ACE = Card.of(CardRank.ACE, CardSymbol.CLOVER); + public static final Card HEART_ACE = Card.of(CardRank.ACE, CardSymbol.HEART); + public static final Card CLOVER2 = Card.of(CardRank.TWO, CardSymbol.CLOVER); + public static final Card CLOVER3 = Card.of(CardRank.THREE, CardSymbol.CLOVER); + public static final Card CLOVER4 = Card.of(CardRank.FOUR, CardSymbol.CLOVER); + public static final Card CLOVER5 = Card.of(CardRank.FIVE, CardSymbol.CLOVER); + public static final Card CLOVER6 = Card.of(CardRank.SIX, CardSymbol.CLOVER); + public static final Card CLOVER7 = Card.of(CardRank.SEVEN, CardSymbol.CLOVER); + public static final Card CLOVER8 = Card.of(CardRank.EIGHT, CardSymbol.CLOVER); + public static final Card CLOVER9 = Card.of(CardRank.NINE, CardSymbol.CLOVER); + public static final Card CLOVER10 = Card.of(CardRank.TEN, CardSymbol.CLOVER); + public static final Card CLOVER_KING = Card.of(CardRank.KING, CardSymbol.CLOVER); + public static final Card CLOVER_QUEEN = Card.of(CardRank.QUEEN, CardSymbol.CLOVER); +} diff --git a/src/test/java/blackjack/domain/fixture/CardStackGenerator.java b/src/test/java/blackjack/domain/fixture/CardStackGenerator.java new file mode 100644 index 0000000000..6af78bc2be --- /dev/null +++ b/src/test/java/blackjack/domain/fixture/CardStackGenerator.java @@ -0,0 +1,30 @@ +package blackjack.domain.fixture; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardStack; +import java.util.LinkedList; +import java.util.List; + +public class CardStackGenerator { + + public static CardStack ofReverse(Card... cards) { + return new CardStackStub(List.of(cards)); + } + + private static class CardStackStub implements CardStack { + final LinkedList cards = new LinkedList<>(); + + CardStackStub(List cards) { + this.cards.addAll(cards); + } + + public Card pop() { + Card newCard = cards.pollFirst(); + if (newCard == null) { + throw new IllegalArgumentException("카드 스택이 비어있습니다!"); + } + + return newCard; + } + } +} diff --git a/src/test/java/blackjack/domain/game/BlackjackGameTest.java b/src/test/java/blackjack/domain/game/BlackjackGameTest.java new file mode 100644 index 0000000000..62a21481d0 --- /dev/null +++ b/src/test/java/blackjack/domain/game/BlackjackGameTest.java @@ -0,0 +1,132 @@ +package blackjack.domain.game; + +import static blackjack.domain.fixture.CardRepository.CLOVER10; +import static blackjack.domain.fixture.CardRepository.CLOVER2; +import static blackjack.domain.fixture.CardRepository.CLOVER3; +import static blackjack.domain.fixture.CardRepository.CLOVER4; +import static blackjack.domain.fixture.CardRepository.CLOVER5; +import static blackjack.domain.fixture.CardRepository.CLOVER6; +import static blackjack.domain.fixture.CardRepository.CLOVER7; +import static blackjack.domain.fixture.CardRepository.CLOVER8; +import static blackjack.domain.fixture.CardRepository.CLOVER9; +import static blackjack.domain.fixture.CardRepository.CLOVER_KING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardDeck; +import blackjack.domain.card.CardStack; +import blackjack.domain.fixture.CardStackGenerator; +import blackjack.domain.participant.Player; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BlackjackGameTest { + + private static final List playerNames = List.of("hudi", "jeong"); + + @DisplayName("생성자는 1명 이상의 플레이어명을 가변 인자로 받아 게임을 생성한다.") + @Test + void constructor_initsGameWithPlayerNames() { + BlackjackGame blackjackGame = new BlackjackGame(new CardDeck(), playerNames); + + List players = blackjackGame.getPlayers(); + + assertThat(players.size()).isEqualTo(2); + assertThat(players.get(0).getName()).isEqualTo("hudi"); + assertThat(players.get(1).getName()).isEqualTo("jeong"); + } + + @DisplayName("생성자에 플레이어명이 입력되지 않으면 예외가 발생한다.") + @Test + void constructor_throwsExceptionOnNoPlayerNameInput() { + assertThatThrownBy(() -> new BlackjackGame(new CardDeck(), List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("플레이어가 없는 게임은 존재할 수 없습니다."); + } + + @DisplayName("생성자에 중복된 플레이어명이 전달되면 예외가 발생한다.") + @Test + void constructor_throwsExceptionOnDuplicateNameInput() { + assertThatThrownBy(() -> new BlackjackGame(new CardDeck(), List.of("hudi", "jeong", "hudi"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("플레이어의 이름은 중복될 수 없습니다."); + } + + @DisplayName("giveExtraCardToPlayer 는 전달받은 플레이어에 카드를 추가하고, 카드를 더 받을 수 있다면 true 를 반환한다.") + @Test + void giveExtraCardToPlayer_addCardToHandOfPlayerAndReturnTrueIfPlayerCanReceiveMore() { + // given + CardStack cards = CardStackGenerator.ofReverse( + CLOVER2, CLOVER3, CLOVER4, CLOVER5, CLOVER6, CLOVER7, CLOVER8); + BlackjackGame blackjackGame = new BlackjackGame(cards, playerNames); + Player player = blackjackGame.getPlayers().get(0); + + // when + boolean actual = blackjackGame.giveExtraCardToPlayer(player); + + // then + assertThat(actual).isTrue(); + } + + @DisplayName("giveExtraCardToPlayer 는 전달받은 플레이어에 카드를 추가하고, 카드를 더 받을 수 없다면 false 를 반환한다.") + @Test + void giveExtraCardToPlayer_addCardToHandOfPlayerAndReturnFalseIfPlayerCanNotReceiveMore() { + // given + CardStack cards = CardStackGenerator.ofReverse( + CLOVER2, CLOVER3, CLOVER10, CLOVER5, CLOVER6, CLOVER7, CLOVER8); + BlackjackGame blackjackGame = new BlackjackGame(cards, playerNames); + Player player = blackjackGame.getPlayers().get(0); + + // when + boolean actual = blackjackGame.giveExtraCardToPlayer(player); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("딜러의 점수가 16이하인 점수가 17이상이 될 때까지 카드를 받는다.") + @Test + void giveExtraCardsToDealer_giveExtraCardsToDealerIfDealerScoreIsLessOrEqualThan16() { + // given + CardStack cards = CardStackGenerator.ofReverse( + CLOVER2, CLOVER3, CLOVER7, CLOVER8, CLOVER9, CLOVER10, CLOVER4, CLOVER5, CLOVER6); + BlackjackGame blackjackGame = new BlackjackGame(cards, playerNames); + + // when + int actual = blackjackGame.giveExtraCardsToDealer(); + int expected = 3; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("딜러의 점수가 17이상인 경우 한 장의 카드를 더 받지 않는다.") + @Test + void giveExtraCardsToDealer_doNotGiveExtraCardsToDealerIfDealerScoreIsGreaterThan16() { + // given + CardStack cards = CardStackGenerator.ofReverse( + CLOVER_KING, CLOVER10, CLOVER7, CLOVER8, CLOVER9, CLOVER4, CLOVER5, CLOVER6); + BlackjackGame blackjackGame = new BlackjackGame(cards, playerNames); + + // when + int actual = blackjackGame.giveExtraCardsToDealer(); + int expected = 0; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("CardDeck 에서 카드 한장을 뽑아서 반환한다.") + @Test + void popCard_returnPoppedCard() { + CardStack cards = CardStackGenerator.ofReverse( + CLOVER6, CLOVER10, CLOVER2, CLOVER3, CLOVER4, CLOVER5, CLOVER_KING); + BlackjackGame blackjackGame = new BlackjackGame(cards, playerNames); + + Card actual = blackjackGame.popCard(); + + assertThat(actual).isEqualTo(CLOVER_KING); + } +} diff --git a/src/test/java/blackjack/domain/game/RefereeTest.java b/src/test/java/blackjack/domain/game/RefereeTest.java new file mode 100644 index 0000000000..626243f6a0 --- /dev/null +++ b/src/test/java/blackjack/domain/game/RefereeTest.java @@ -0,0 +1,84 @@ +package blackjack.domain.game; + +import static blackjack.domain.fixture.CardRepository.CLOVER10; +import static blackjack.domain.fixture.CardRepository.CLOVER7; +import static blackjack.domain.fixture.CardRepository.CLOVER8; +import static blackjack.domain.fixture.CardRepository.CLOVER9; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Hand; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class RefereeTest { + + @DisplayName("generateDealerResult 에 Player 리스트와 Dealer 를 전달하면, Dealer 의 승패무 결과를 반환한다.") + @Test + void generateDealerResult() { + // given + Referee referee = new Referee(); + Dealer dealer = Dealer.of(Hand.of(CLOVER9, CLOVER7)); + Player winnerPlayer = Player.of("winner", Hand.of(CLOVER10, CLOVER7)); + Player loserPlayer = Player.of("loser", Hand.of(CLOVER8, CLOVER7)); + + // when + Map result = referee.generateDealerResult(dealer, Set.of(winnerPlayer, loserPlayer)); + + // then + assertThat(result.get(ResultType.WIN)).isEqualTo(new ResultCount(1)); + assertThat(result.get(ResultType.LOSE)).isEqualTo(new ResultCount(1)); + assertThat(result.get(ResultType.DRAW)).isEqualTo(new ResultCount(0)); + } + + @DisplayName("generatePlayerResult 에 점수가 높은 플레이어와 낮은 플레이어를 전달하면, ResultType.WIN 을 반환한다.") + @Test + void generatePlayerResult_returnsWin() { + // given + Referee referee = new Referee(); + Player winnerPlayer = Player.of("winner", Hand.of(CLOVER10, CLOVER7)); + Player loserPlayer = Player.of("loser", Hand.of(CLOVER8, CLOVER7)); + + // when + ResultType actual = referee.generatePlayerResult(winnerPlayer, loserPlayer); + ResultType expected = ResultType.WIN; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("generatePlayerResult 에 점수가 낮은 플레이어와 높은 플레이어를 전달하면, ResultType.LOSE 을 반환한다.") + @Test + void generatePlayerResult_returnsLose() { + // given + Referee referee = new Referee(); + Player winnerPlayer = Player.of("winner", Hand.of(CLOVER10, CLOVER7)); + Player loserPlayer = Player.of("loser", Hand.of(CLOVER8, CLOVER7)); + + // when + ResultType actual = referee.generatePlayerResult(loserPlayer, winnerPlayer); + ResultType expected = ResultType.LOSE; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("generatePlayerResult 에 점수가 같은 두 플레이어를 전달하면, ResultType.DRAW 을 반환한다.") + @Test + void generatePlayerResult_returnsDraw() { + // given + Referee referee = new Referee(); + Player player1 = Player.of("winner", Hand.of(CLOVER10, CLOVER7)); + Player player2 = Player.of("loser", Hand.of(CLOVER9, CLOVER8)); + + // when + ResultType actual = referee.generatePlayerResult(player1, player2); + ResultType expected = ResultType.DRAW; + + // then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/game/ResultCountTest.java b/src/test/java/blackjack/domain/game/ResultCountTest.java new file mode 100644 index 0000000000..af8df4b8df --- /dev/null +++ b/src/test/java/blackjack/domain/game/ResultCountTest.java @@ -0,0 +1,59 @@ +package blackjack.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class ResultCountTest { + @DisplayName("생성자에 매개변수를 전달하지 않으면 value 값은 0으로 초기화된다.") + @Test + void constructor_initsWithZero() { + ResultCount resultCount = new ResultCount(); + + int actual = resultCount.getValue(); + + assertThat(actual).isZero(); + } + + @DisplayName("생성자에 int 값을 전달하면 value 는 전달된 값으로 초기화된다.") + @ParameterizedTest(name = "[{index}] 값: {0}") + @ValueSource(ints = {1, 10, 100}) + void constructor_withIntValue(int input) { + // given + ResultCount resultCount = new ResultCount(input); + + // when + int actual = resultCount.getValue(); + + // then + assertThat(actual).isEqualTo(input); + } + + @DisplayName("같은 value 를 가지고 있는 서로 다른 ResultCount 는 동등하다.") + @ParameterizedTest(name = "[{index}] 값: {0}") + @ValueSource(ints = {1, 10, 100}) + void resultCountEquality(int input) { + // given + ResultCount resultCount = new ResultCount(input); + ResultCount anotherHasSameValue = new ResultCount(input); + + // when & then + assertThat(resultCount).isEqualTo(anotherHasSameValue); + } + + @DisplayName("increment 를 통해 값을 1씩 증가시킬 수 있다.") + @Test + void increment() { + ResultCount resultCount = new ResultCount(); + for (int i = 0; i < 3; i++) { + resultCount.increment(); + } + + int actual = resultCount.getValue(); + + assertThat(actual).isEqualTo(3); + } +} diff --git a/src/test/java/blackjack/domain/game/ResultTypeTest.java b/src/test/java/blackjack/domain/game/ResultTypeTest.java new file mode 100644 index 0000000000..3dc6acb6f2 --- /dev/null +++ b/src/test/java/blackjack/domain/game/ResultTypeTest.java @@ -0,0 +1,31 @@ +package blackjack.domain.game; + +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 ResultTypeTest { + + private static final int TYPES_LENGTH = 3; + + @DisplayName("WIN, LOSE, DRAW 3개의 인스턴스가 생성된다.") + @Test + void init() { + int actual = ResultType.values().length; + assertThat(actual).isEqualTo(TYPES_LENGTH); + } + + @DisplayName("WIN, LOSE, DRAW 인스턴스는 각각 승, 패, 무라는 이름을 갖는다.") + @Test + void getDisplayName() { + List types = List.of(ResultType.values()); + List names = List.of("승", "패", "무"); + + for (int i = 0; i < TYPES_LENGTH; i++) { + assertThat(types.get(i).getDisplayName()) + .isEqualTo(names.get(i)); + } + } +} diff --git a/src/test/java/blackjack/domain/game/ScoreTest.java b/src/test/java/blackjack/domain/game/ScoreTest.java new file mode 100644 index 0000000000..6ea37b5b75 --- /dev/null +++ b/src/test/java/blackjack/domain/game/ScoreTest.java @@ -0,0 +1,75 @@ +package blackjack.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class ScoreTest { + + @DisplayName("정적 팩토리 메소드 valueOf 로 새로운 점수 인스턴스를 생성한다.") + @Test + void valueOf_createNewScore() { + Score score = Score.valueOf(1); + + assertThat(score).isNotNull(); + } + + @DisplayName("정적 팩토리 메소드 valueOf 는 캐싱된 점수 인스턴스를 가져온다.") + @Test + void valueOf_getCache() { + Score score = Score.valueOf(10); + Score sameScore = Score.valueOf(10); + + assertThat(score).isEqualTo(sameScore); + } + + @DisplayName("인스턴스 메서드 add 는 다른 Score 인스턴스를 받아 더한 값의 Score 인스턴스를 반환한다.") + @Test + void add() { + Score score = Score.valueOf(10); + Score anotherScore = Score.valueOf(15); + + Score newScore = score.add(anotherScore); + + assertThat(newScore.getValue()).isEqualTo(25); + } + + @DisplayName("compareTo 는 각 점수 인스턴스의 크기를 비교한다.") + @Nested + class CompareToTest { + @DisplayName("기준 인스턴스의 value 값이 비교 대상보다 더 클 경우 양수를 반환한다.") + @Test + void compareTo_returnPositiveIfBiggerThanTarget() { + Score score = Score.valueOf(15); + Score smallerScore = Score.valueOf(10); + + int actual = score.compareTo(smallerScore); + + assertThat(actual).isPositive(); + } + + @DisplayName("기준 인스턴스의 value 값이 비교 대상과 동일한 경우 0을 반환한다.") + @Test + void compareTo_returnZeroIfSameAsTarget() { + Score score = Score.valueOf(10); + Score sameScore = Score.valueOf(10); + + int actual = score.compareTo(sameScore); + + assertThat(actual).isZero(); + } + + @DisplayName("기준 인스턴스의 value 값이 비교 대상보다 더 작을 경우 음수를 반환한다.") + @Test + void compareTo_returnNegativeIfSmallerThanTarget() { + Score score = Score.valueOf(15); + Score biggerScore = Score.valueOf(20); + + int actual = score.compareTo(biggerScore); + + assertThat(actual).isNegative(); + } + } +} 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 0000000000..ba4be83e91 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,118 @@ +package blackjack.domain.participant; + +import static blackjack.domain.fixture.CardRepository.CLOVER2; +import static blackjack.domain.fixture.CardRepository.CLOVER3; +import static blackjack.domain.fixture.CardRepository.CLOVER4; +import static blackjack.domain.fixture.CardRepository.CLOVER5; +import static blackjack.domain.fixture.CardRepository.CLOVER6; +import static blackjack.domain.fixture.CardRepository.CLOVER7; +import static blackjack.domain.fixture.CardRepository.CLOVER8; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.game.ResultType; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DealerTest { + + private Dealer dealer; + + @BeforeEach + void setUp() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + dealer = Dealer.of(hand); + } + + @DisplayName("Dealer 인스턴스가 생성된다.") + @Test + void of() { + assertThat(dealer).isNotNull(); + } + + @DisplayName("Card 를 전달받아 Hand 에 추가할 수 있다.") + @Test + void receiveCard() { + dealer.receiveCard(CLOVER6); + + Set actual = dealer.getHand().getCards(); + + assertThat(actual).containsExactlyInAnyOrder(CLOVER4, CLOVER5, CLOVER6); + } + + @DisplayName("Score 가 16을 넘지 않으면 true 를 반환한다.") + @Test + void canReceive_returnTrueOnLessThan16() { + dealer.receiveCard(CLOVER6); + boolean actual = dealer.canReceive(); + + assertThat(actual).isTrue(); + } + + @DisplayName("Score 가 16이면 true 를 반환한다.") + @Test + void canReceive_returnTrueOn16() { + dealer.receiveCard(CLOVER7); + + boolean actual = dealer.canReceive(); + + assertThat(actual).isTrue(); + } + + @DisplayName("Score 가 17 이상이면 false 를 반환한다.") + @Test + void canReceive_returnFalseOnGreaterThan16() { + dealer.receiveCard(CLOVER8); + + boolean actual = dealer.canReceive(); + + assertThat(actual).isFalse(); + } + + // TODO: Nested 사용하여 정리 + + @DisplayName("compareWith 메소드는 딜러 자신보다 패가 나쁜 Participant 를 전달받으면 ResultType.WIN 를 반환한다.") + @Test + void compareWith_returnsResultTypeWin() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER2, CLOVER3)); + + // when + ResultType actual = dealer.compareWith(winPlayer); + ResultType expected = ResultType.WIN; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("compareWith 메소드는 딜러 자신보다 패가 좋은 Participant 를 전달받으면 ResultType.LOSE 를 반환한다.") + @Test + void compareWith_returnsResultTypeLose() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER5, CLOVER6)); + + // when + ResultType actual = dealer.compareWith(winPlayer); + ResultType expected = ResultType.LOSE; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("compareWith 메소드는 딜러 자신과 대등한 패를 가진 Participant 를 전달받으면 ResultType.DRAW 를 반환한다.") + @Test + void compareWith_returnsResultTypeDraw() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER3, CLOVER6)); + + // when + ResultType actual = dealer.compareWith(winPlayer); + ResultType expected = ResultType.DRAW; + + // then + assertThat(actual).isEqualTo(expected); + } +} 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 0000000000..10a6554c1f --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,129 @@ +package blackjack.domain.participant; + +import static blackjack.domain.fixture.CardRepository.CLOVER10; +import static blackjack.domain.fixture.CardRepository.CLOVER2; +import static blackjack.domain.fixture.CardRepository.CLOVER3; +import static blackjack.domain.fixture.CardRepository.CLOVER4; +import static blackjack.domain.fixture.CardRepository.CLOVER5; +import static blackjack.domain.fixture.CardRepository.CLOVER6; +import static blackjack.domain.fixture.CardRepository.CLOVER_KING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Hand; +import blackjack.domain.game.ResultType; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PlayerTest { + + private Player player; + + @BeforeEach + void setUp() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + player = Player.of("hudi", hand); + } + + @DisplayName("Player 인스턴스가 생성된다.") + @Test + void of() { + assertThat(player).isNotNull(); + } + + @DisplayName("Player 의 이름이 비어있을 경우 예외가 발생한다.") + @Test + void of_withEmptyNameThrowsIllegalArgumentException() { + Hand hand = Hand.of(CLOVER4, CLOVER5); + assertThatThrownBy(() -> Player.of("", hand)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 1글자 이상이어야합니다."); + } + + @DisplayName("Card 를 전달받아 Hand 에 추가할 수 있다.") + @Test + void receiveCard() { + player.receiveCard(CLOVER6); + + Set actual = player.getHand().getCards(); + + assertThat(actual).containsExactlyInAnyOrder(CLOVER4, CLOVER5, CLOVER6); + } + + @DisplayName("Score 가 21을 넘지 않으면 true 를 반환한다.") + @Test + void canReceive_returnTrueOnLessThan21() { + boolean actual = player.canReceive(); + + assertThat(actual).isTrue(); + } + + @DisplayName("Score 가 21이면 true 를 반환한다.") + @Test + void canReceive_returnTrueOn21() { + player.receiveCard(CLOVER2); + player.receiveCard(CLOVER10); + + boolean actual = player.canReceive(); + + assertThat(actual).isTrue(); + } + + @DisplayName("Score 가 21을 초과하면 false 를 반환한다.") + @Test + void canReceive_returnFalseOnGreaterThan21() { + player.receiveCard(CLOVER10); + player.receiveCard(CLOVER_KING); + + boolean actual = player.canReceive(); + + assertThat(actual).isFalse(); + } + + // TODO: Nested 사용하여 정리 + + @DisplayName("compareWith 메소드는 딜러 자신보다 패가 나쁜 Participant 를 전달받으면 ResultType.WIN 를 반환한다.") + @Test + void compareWith_returnsResultTypeWin() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER2, CLOVER3)); + + // when + ResultType actual = player.compareWith(winPlayer); + ResultType expected = ResultType.WIN; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("compareWith 메소드는 딜러 자신보다 패가 좋은 Participant 를 전달받으면 ResultType.LOSE 를 반환한다.") + @Test + void compareWith_returnsResultTypeLose() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER5, CLOVER6)); + + // when + ResultType actual = player.compareWith(winPlayer); + ResultType expected = ResultType.LOSE; + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("compareWith 메소드는 딜러 자신과 대등한 패를 가진 Participant 를 전달받으면 ResultType.DRAW 를 반환한다.") + @Test + void compareWith_returnsResultTypeDraw() { + // given + Player winPlayer = Player.of("hudi", Hand.of(CLOVER3, CLOVER6)); + + // when + ResultType actual = player.compareWith(winPlayer); + ResultType expected = ResultType.DRAW; + + // then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/fuel/CarTest.java b/src/test/java/fuel/CarTest.java new file mode 100644 index 0000000000..717369a3a4 --- /dev/null +++ b/src/test/java/fuel/CarTest.java @@ -0,0 +1,100 @@ +package fuel; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class CarTest { + + @DisplayName("Car 구현체의 생성자를 호출하면, 객체가 생성된다.") + @Test + void constructor() { + Car car = new Sonata(150); + + assertThat(car).isNotNull(); + } + + @DisplayName("Sonata 구현체 테스트") + @Nested + class SonataTest { + @DisplayName("소나타의 이름은 언제나 Sonata 이다.") + @ParameterizedTest(name = "[{index}] 거리: {0}") + @ValueSource(ints = {100, 500, 3000}) + void getName_sonata(int distance) { + Car car = new Sonata(distance); + + String actual = car.getName(); + + assertThat(actual).isEqualTo("Sonata"); + } + + @DisplayName("소나타는 필요한 연료를 거리와 연비를 통해 계산하여 반환한다.") + @ParameterizedTest(name = "[{index}] 거리: {0}, 필요한 연료: {1}") + @CsvSource(value = {"100,10", "500,50", "3000,300"}) + void getFuelNeeded_sonata(int distance, int expected) { + Car car = new Sonata(distance); + + int actual = car.getFuelNeeded(); + + assertThat(actual).isEqualTo(expected); + } + } + + @DisplayName("K5 구현체 테스트") + @Nested + class K5Test { + @DisplayName("K5의 이름은 언제나 K5 이다.") + @ParameterizedTest(name = "[{index}] 거리: {0}") + @ValueSource(ints = {100, 500, 3000}) + void getName_k5(int distance) { + Car car = new K5(distance); + + String actual = car.getName(); + + assertThat(actual).isEqualTo("K5"); + } + + @DisplayName("K5는 필요한 연료를 거리와 연비를 통해 계산하여 반환한다.") + @ParameterizedTest(name = "[{index}] 거리: {0}, 필요한 연료: {1}") + @CsvSource(value = {"130,10", "260,20", "1300,100"}) + void getFuelNeeded_k5(int distance, int expected) { + Car car = new K5(distance); + + int actual = car.getFuelNeeded(); + + assertThat(actual).isEqualTo(expected); + } + } + + @DisplayName("Avante 구현체 테스트") + @Nested + class AvanteTest { + @DisplayName("아반떼의 이름은 언제나 Avante 이다.") + @ParameterizedTest(name = "[{index}] 거리: {0}") + @ValueSource(ints = {100, 500, 3000}) + void getName_avante(int distance) { + Car car = new Avante(distance); + + String actual = car.getName(); + + assertThat(actual).isEqualTo("Avante"); + } + + @DisplayName("아반떼는 필요한 연료를 거리와 연비를 통해 계산하여 반환한다.") + @ParameterizedTest(name = "[{index}] 거리: {0}, 필요한 연료: {1}") + @CsvSource(value = {"150,10", "300,20", "1500,100"}) + void getFuelNeeded_avante(int distance, int expected) { + Car car = new Avante(distance); + + int actual = car.getFuelNeeded(); + + assertThat(actual).isEqualTo(expected); + } + } + +} diff --git a/src/test/java/fuel/RentCompanyTest.java b/src/test/java/fuel/RentCompanyTest.java new file mode 100644 index 0000000000..94e0b71b67 --- /dev/null +++ b/src/test/java/fuel/RentCompanyTest.java @@ -0,0 +1,60 @@ +package fuel; + +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 RentCompanyTest { + private static final String NEWLINE = System.lineSeparator(); + + @DisplayName("create 팩토리 메소드로 RentCompany 인스턴스를 생성할 수 있다.") + @Test + void create() { + RentCompany company = RentCompany.create(); + + assertThat(company).isExactlyInstanceOf(RentCompany.class); + } + + @DisplayName("create 팩토리 메소드는 매번 다른 RentCompany 인스턴스를 생성한다.") + @Test + void create_isNotSingleton() { + RentCompany company = RentCompany.create(); + RentCompany anotherCompany = RentCompany.create(); + + assertThat(company).isNotEqualTo(anotherCompany); + } + + @DisplayName("addCar 를 통해 회사에 자동차를 추가할 수 있다.") + @Test + void addCar() { + RentCompany company = RentCompany.create(); + company.addCar(new Sonata(150)); + company.addCar(new K5(250)); + List currentCars = company.addCar(new Avante(1000)); + + assertThat(currentCars.size()).isEqualTo(3); + } + + @DisplayName("generateReport 를 통해 각 차량 별로 주입해야 할 연료량 정보를 반환한다.") + @Test + void generateReport() { + RentCompany company = RentCompany.create(); + company.addCar(new Sonata(150)); + company.addCar(new K5(260)); + company.addCar(new Sonata(120)); + company.addCar(new Avante(300)); + company.addCar(new K5(390)); + + String report = company.generateReport(); + + assertThat(report).isEqualTo( + "Sonata : 15리터" + NEWLINE + + "K5 : 20리터" + NEWLINE + + "Sonata : 12리터" + NEWLINE + + "Avante : 20리터" + NEWLINE + + "K5 : 30리터" + NEWLINE + ); + } +}