diff --git a/README.md b/README.md index 556099c4de3..82fa370423e 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,105 @@ ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) + +```mermaid +--- +title: 의존성그래프 +--- +graph TD +BlackJackGame --> Players +BlackJackGame --> Deck +BlackJackGame --> Dealer + +Players --> Dealer +Dealer --> Player + +Participant --> CardPocket +Dealer --> Participant +Player --> Participant + +Players --> Player +Player --> Name +CardPocket --> Card + +Deck --> Card +Card --> Shape +Card --> Symbol +``` + +# 기능구현 목록 + +## UI + +1. 입력 + - [x] 베팅 금액을 입력받는다 +2. 출력 + - [x] 최종 수익을 출력한다 + +## 도메인 + +1. BettingMoney + - [x] 사용자에 베팅 금액을 받는다 + - [예외처리] 0 이상의 숫자인지 확인한다 + - [x] 베팅 금액에 대한 수익률을 계산한다 +2. CardPocket + - Card 와 관련된 도메인에 대한 애그리거트 루트입니다 + - 도메인 외부로 조회된 결과를 반환할 때 CardResponse 형태로 바꾸어 반환합니다 +3. Participants + - 플레이어와 딜러에 대한 애그리거트 루트입니다 + - 외부에서 발생하는 모든 조회는 Participant 를 통해 조회해야합니다 +4. ResultType + - 블랙잭 게임과 관련된 결과를 담당하는 클래스 입니다 +5. BLackjackRule + - 블랙잭 게임의 결과를 계산하는 도메인 서비스입니다 + - Player 와, Dealer 를 받아서 수익률을 계산합니다 + +## UI + +1. 입력 + +- 참여할 사람을 입력한다 +- List으로 변환 및 반환한다 + +## 도메인 + +1. Deck + - [x] 카드를 생성한다 + - [x] 총 52장 + - [x] Shape마다 13장을 출력 + - [x] 카드를 나눠준다 + +2. BlackJack + - 딜러와 플레이어에게 카드를 2장씩 나눠준다. + -- 카드를 나눠준다 +3. Participant + - [x] 21 이상인지 확인하고 + - [x] 21 이상이라면, + - [x] 블랙잭인지 확인한다 = 받을 수 없음 + - [x] BURST인지 확인한다 = 받을 수 없음4 + - [x] 현재 점수를 숫자 형태로 반환한다 + - [x] 현재 카드를 반환한다 +4. Player + - [x] 21 미만이면, 받을 수 있다는 boolean +5. Dealer + - [x] 16 이하이면, 받을 수 있따는 boolean +6. CardPocket + - [x] 카드의 Symbol 점수를 계산한다.(ScoreCalculator 역할) + - 카드반환 한다 + - [x] 카드 계산 총합 반환 +7. Shape + - [x] enum 형태로 하트, 스페이스, 클로버, 다이아를 저장한다 +8. Symbol + - [x] a=1 2-9, j,q,k =10 + - [x] value는 int 형으로 저장한다 +9. Card + - [x] Shape와 Symbol을 저장하는 자료구조 + +10. Players + - [x] 플레이어 이름 중복 + - [x] 플레이어 수 1명이상, 5명 이하 + - [x] 플레이어에게 카드 나눠주기 + - [x] 플레이어의 draw여부 알기 +11. Name + - [x] isBlank 체크 + - [x] 100자 이하 체크 diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..57ab2e6205c --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,17 @@ +package blackjack; + +import blackjack.controller.BlackjackController; +import blackjack.view.IOView; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class Application { + + public static void main(final String[] args) { + final IOView ioView = new IOView(new InputView(), new OutputView()); + final BlackjackController blackjackController = new BlackjackController(ioView); + blackjackController.init(); + blackjackController.play(); + blackjackController.calculateResult(); + } +} diff --git a/src/main/java/blackjack/controller/BlackjackController.java b/src/main/java/blackjack/controller/BlackjackController.java new file mode 100644 index 00000000000..625f4523572 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,96 @@ +package blackjack.controller; + +import blackjack.domain.blackjack.BlackjackGame; +import blackjack.domain.card.ShuffledDeckFactory; +import blackjack.domain.participant.Participants; +import blackjack.view.DrawCommand; +import blackjack.view.IOView; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class BlackjackController { + + private final IOView ioView; + private BlackjackGame blackjackGame; + + public BlackjackController(final IOView ioView) { + this.ioView = ioView; + } + + public void init() { + final List playerNames = inputPlayerNames(); + final List moneys = new ArrayList<>(); + for (final String playerName : playerNames) { + moneys.add(inputPlayerMoney(playerName)); + } + blackjackGame = BlackjackGame.of(playerNames, moneys, new ShuffledDeckFactory().generate()); + blackjackGame.distributeInitialCards(); + ioView.printInitialCards(blackjackGame.getDealerFirstCard(), blackjackGame.getPlayersCards()); + } + + public void play() { + for (final String playerName : blackjackGame.getPlayerNames()) { + drawPlayerCards(playerName); + } + while (blackjackGame.isDealerDrawable()) { + blackjackGame.drawDealerCard(); + ioView.printDealerCardDrawMessage(); + } + } + + public void calculateResult() { + ioView.printFinalStatusOfDealer(blackjackGame.getDealerScore(), blackjackGame.getDealerCards()); + ioView.printFinalStatusOfPlayers(blackjackGame.getPlayersCards(), blackjackGame.getPlayersScores()); + ioView.printFinalMoney(blackjackGame.calculateMoney()); + } + + private void drawPlayerCards(final String playerName) { + DrawCommand playerChoice = DrawCommand.DRAW; + while (blackjackGame.isPlayerDrawable(playerName) && playerChoice != DrawCommand.STAY) { + playerChoice = inputPlayerChoice(playerName); + drawPlayerCard(playerName, playerChoice); + ioView.printCardStatusOfPlayer(playerName, blackjackGame.getPlayerCards(playerName)); + } + } + + private void drawPlayerCard(final String playerName, final DrawCommand playerChoice) { + if (playerChoice == DrawCommand.DRAW) { + blackjackGame.drawPlayerCard(playerName); + } + } + + private List inputPlayerNames() { + return repeatUntilNoException(() -> { + final List names = ioView.inputPlayerNames(); + Participants.validatePlayerNames(names); + return names; + }, + ioView::printError); + } + + private int inputPlayerMoney(final String playerName) { + return repeatUntilNoException(() -> { + final int amount = ioView.inputPlayerMoney(playerName); + Participants.validateBettingMoney(amount); + return amount; + }, + ioView::printError); + } + + private DrawCommand inputPlayerChoice(final String playerName) { + return repeatUntilNoException(() -> ioView.inputCommand(playerName), ioView::printError); + } + + private T repeatUntilNoException(final Supplier supplier, + final Consumer exceptionHandler) { + while (true) { + try { + return supplier.get(); + } catch (final IllegalArgumentException e) { + exceptionHandler.accept(e); + } + } + } +} diff --git a/src/main/java/blackjack/domain/blackjack/BlackJackRule.java b/src/main/java/blackjack/domain/blackjack/BlackJackRule.java new file mode 100644 index 00000000000..5030b0b2a5b --- /dev/null +++ b/src/main/java/blackjack/domain/blackjack/BlackJackRule.java @@ -0,0 +1,51 @@ +package blackjack.domain.blackjack; + +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; + +class BlackJackRule { + + private static final int BUST_POINT = 21; + + int calculatePlayerProfit(final Player player, final Dealer dealer) { + final ResultType result = calculatePlayerResult(dealer, player); + return player.calculateProfit(result.getPlayerProfit()); + } + + private ResultType calculatePlayerResult(final Dealer dealer, final Player player) { + if (dealer.hasBlackJack()) { + return playWithBlackjack(player); + } + if (dealer.currentScore() > BUST_POINT) { + return playWithBust(player); + } + return playWithScore(dealer, player); + } + + private ResultType playWithBlackjack(final Player player) { + if (player.hasBlackJack()) { + return ResultType.TIE; + } + return ResultType.BLACKJACK_LOSE; + } + + private ResultType playWithBust(final Player player) { + if (player.currentScore() > BUST_POINT) { + return ResultType.TIE; + } + if (player.hasBlackJack()) { + return ResultType.BLACKJACK_WIN; + } + return ResultType.WIN; + } + + private ResultType playWithScore(final Dealer dealer, final Player player) { + if (player.hasBlackJack()) { + return ResultType.BLACKJACK_WIN; + } + if (player.currentScore() > BUST_POINT || dealer.currentScore() > player.currentScore()) { + return ResultType.LOSE; + } + return ResultType.WIN; + } +} diff --git a/src/main/java/blackjack/domain/blackjack/BlackjackGame.java b/src/main/java/blackjack/domain/blackjack/BlackjackGame.java new file mode 100644 index 00000000000..0dbc4fd94e9 --- /dev/null +++ b/src/main/java/blackjack/domain/blackjack/BlackjackGame.java @@ -0,0 +1,82 @@ +package blackjack.domain.blackjack; + +import blackjack.domain.card.Deck; +import blackjack.domain.card.dto.CardResponse; +import blackjack.domain.participant.Participants; +import blackjack.domain.participant.Player; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class BlackjackGame { + + private final Participants participants; + private final Deck deck; + + private BlackjackGame(final Participants participants, final Deck deck) { + this.participants = participants; + this.deck = deck; + } + + public static BlackjackGame of(final List playerNames, final List bettingMoneys, final Deck deck) { + return new BlackjackGame(Participants.of(playerNames, bettingMoneys), deck); + } + + public void distributeInitialCards() { + participants.distributeInitialCards(deck); + } + + public CardResponse getDealerFirstCard() { + return participants.getDealerFirstCard(); + } + + public Map> getPlayersCards() { + return participants.getPlayersCards(); + } + + public List getPlayerNames() { + return participants.getPlayerNames(); + } + + public boolean isDealerDrawable() { + return participants.isDealerDrawable(); + } + + public void drawDealerCard() { + participants.drawDealerCard(deck.popCard()); + } + + public boolean isPlayerDrawable(final String playerName) { + return participants.isPlayerDrawable(playerName); + } + + public List getPlayerCards(final String playerName) { + return participants.getPlayerCards(playerName); + } + + public void drawPlayerCard(final String playerName) { + participants.drawPlayerCard(playerName, deck.popCard()); + } + + public int getDealerScore() { + return participants.getDealerScore(); + } + + public List getDealerCards() { + return participants.getDealerCards(); + } + + public Map getPlayersScores() { + return participants.getPlayersScores(); + } + + public Map calculateMoney() { + final Map playerMoney = new LinkedHashMap<>(); + final BlackJackRule blackJackRule = new BlackJackRule(); + for (final Player player : participants.getPlayers()) { + final int resultMoney = blackJackRule.calculatePlayerProfit(player, participants.getDealer()); + playerMoney.put(player.getName(), resultMoney); + } + return playerMoney; + } +} diff --git a/src/main/java/blackjack/domain/blackjack/ResultType.java b/src/main/java/blackjack/domain/blackjack/ResultType.java new file mode 100644 index 00000000000..89631514952 --- /dev/null +++ b/src/main/java/blackjack/domain/blackjack/ResultType.java @@ -0,0 +1,19 @@ +package blackjack.domain.blackjack; + +public enum ResultType { + BLACKJACK_WIN(1.5), + WIN(1), + TIE(0), + LOSE(-1), + BLACKJACK_LOSE(-1); + + private final double playerProfit; + + ResultType(final double playerProfit) { + this.playerProfit = playerProfit; + } + + public double getPlayerProfit() { + return playerProfit; + } +} 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..5134c1f77bd --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,28 @@ +package blackjack.domain.card; + +public class Card { + + private final Shape shape; + private final Symbol symbol; + + public Card(final Shape shape, final Symbol symbol) { + this.shape = shape; + this.symbol = symbol; + } + + boolean isAce() { + return symbol.isAce(); + } + + int getScore() { + return symbol.getScore(); + } + + public Symbol getSymbol() { + return symbol; + } + + public Shape getShape() { + return shape; + } +} diff --git a/src/main/java/blackjack/domain/card/CardPocket.java b/src/main/java/blackjack/domain/card/CardPocket.java new file mode 100644 index 00000000000..f266827ef1e --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardPocket.java @@ -0,0 +1,77 @@ +package blackjack.domain.card; + +import blackjack.domain.card.dto.CardResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CardPocket { + + private static final int BUST_SCORE = 21; + private static final int BLACKJACK_SCORE = 21; + private static final int VALUE_ACE = 10; + + private final List cards; + + private CardPocket(final List cards) { + validateCardPocket(cards); + this.cards = new ArrayList<>(cards); + } + + public static CardPocket empty() { + return new CardPocket(new ArrayList<>()); + } + + private void validateCardPocket(final List cards) { + if (cards == null) { + throw new IllegalArgumentException("카드에 null 값이 들어갈 수 없습니다"); + } + } + + public void addCard(final Card card) { + cards.add(card); + calculateCurrentScore(); + } + + private int calculateCurrentScore() { + final int countOfAce = countAce(); + int scoreOfCards = calculateMinimumScore(); + for (int i = 0; i < countOfAce; i++) { + scoreOfCards = calculateAceScore(scoreOfCards); + } + return scoreOfCards; + } + + public int calculateScore() { + return calculateCurrentScore(); + } + + private int countAce() { + return (int) cards.stream() + .filter(Card::isAce) + .count(); + } + + private int calculateMinimumScore() { + return cards.stream() + .mapToInt(Card::getScore) + .sum(); + } + + private int calculateAceScore(final int score) { + if (score + VALUE_ACE > BUST_SCORE) { + return score; + } + return score + VALUE_ACE; + } + + public List getCards() { + return cards.stream() + .map(CardResponse::from) + .collect(Collectors.toList()); + } + + public boolean isBlackJack() { + return cards.size() == 2 && calculateScore() == BLACKJACK_SCORE; + } +} 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..981601cf776 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,32 @@ +package blackjack.domain.card; + +import java.util.Queue; + +public class Deck { + + private static final int NUMBER_OF_CARDS_IN_DECK = 52; + + private final Queue cards; + + Deck(final Queue cards) { + validateCards(cards); + this.cards = cards; + } + + private void validateCards(final Queue cards) { + if (cards == null) { + throw new IllegalArgumentException("카드에 null 이 들어왔습니다"); + } + if (cards.size() != NUMBER_OF_CARDS_IN_DECK) { + throw new IllegalArgumentException( + "카드 숫자는 " + NUMBER_OF_CARDS_IN_DECK + "장이어야 합니다 현재 :" + cards.size() + "장"); + } + } + + public Card popCard() { + if (cards.isEmpty()) { + throw new IllegalArgumentException("덱에 카드가 없습니다"); + } + return cards.remove(); + } +} diff --git a/src/main/java/blackjack/domain/card/DeckFactory.java b/src/main/java/blackjack/domain/card/DeckFactory.java new file mode 100644 index 00000000000..3ca5497cae1 --- /dev/null +++ b/src/main/java/blackjack/domain/card/DeckFactory.java @@ -0,0 +1,6 @@ +package blackjack.domain.card; + +public interface DeckFactory { + + Deck generate(); +} 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..446b373e9e4 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Shape.java @@ -0,0 +1,17 @@ +package blackjack.domain.card; + +public enum Shape { + HEART("하트"), + DIAMOND("다이아몬드"), + SPADE("스페이드"), + CLOVER("클로버"); + private final String name; + + Shape(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/card/ShuffledDeckFactory.java b/src/main/java/blackjack/domain/card/ShuffledDeckFactory.java new file mode 100644 index 00000000000..a00516b0c96 --- /dev/null +++ b/src/main/java/blackjack/domain/card/ShuffledDeckFactory.java @@ -0,0 +1,25 @@ +package blackjack.domain.card; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ShuffledDeckFactory implements DeckFactory { + + @Override + public Deck generate() { + final List cards = generateCards(); + Collections.shuffle(cards); + + return new Deck(new ArrayDeque<>(cards)); + } + + private List generateCards() { + return Arrays.stream(Shape.values()) + .flatMap(shape -> Arrays.stream(Symbol.values()) + .map(symbol -> new Card(shape, symbol))) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/blackjack/domain/card/Symbol.java b/src/main/java/blackjack/domain/card/Symbol.java new file mode 100644 index 00000000000..15f48a57a3b --- /dev/null +++ b/src/main/java/blackjack/domain/card/Symbol.java @@ -0,0 +1,37 @@ +package blackjack.domain.card; + +public enum Symbol { + ACE(1, "A"), + TWO(2, "2"), + THREE(3, "3"), + FOUR(4, "4"), + FIVE(5, "5"), + SIX(6, "6"), + SEVEN(7, "7"), + EIGHT(8, "8"), + NINE(9, "9"), + TEN(10, "10"), + JACK(10, "J"), + QUEEN(10, "Q"), + KING(10, "K"); + + private final int score; + private final String name; + + Symbol(final int score, final String name) { + this.score = score; + this.name = name; + } + + public int getScore() { + return score; + } + + public boolean isAce() { + return this == ACE; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/card/dto/CardResponse.java b/src/main/java/blackjack/domain/card/dto/CardResponse.java new file mode 100644 index 00000000000..eaacb09c2af --- /dev/null +++ b/src/main/java/blackjack/domain/card/dto/CardResponse.java @@ -0,0 +1,52 @@ +package blackjack.domain.card.dto; + +import blackjack.domain.card.Card; +import java.util.Objects; + +public class CardResponse { + + private final String symbol; + private final String shape; + + private CardResponse(final String symbol, final String shape) { + this.symbol = symbol; + this.shape = shape; + } + + public static CardResponse from(final Card card) { + return new CardResponse(card.getSymbol().getName(), card.getShape().getName()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CardResponse that = (CardResponse) o; + return Objects.equals(symbol, that.symbol) && Objects.equals(shape, that.shape); + } + + @Override + public int hashCode() { + return Objects.hash(symbol, shape); + } + + @Override + public String toString() { + return "CardResponse{" + + "symbol='" + symbol + '\'' + + ", shape='" + shape + '\'' + + '}'; + } + + public String getSymbol() { + return symbol; + } + + public String getShape() { + return shape; + } +} diff --git a/src/main/java/blackjack/domain/participant/BettingMoney.java b/src/main/java/blackjack/domain/participant/BettingMoney.java new file mode 100644 index 00000000000..6782e3fd844 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/BettingMoney.java @@ -0,0 +1,17 @@ +package blackjack.domain.participant; + +class BettingMoney { + + private final int value; + + BettingMoney(final int value) { + if (value < 0) { + throw new IllegalArgumentException("베팅 금액은 0보다 작을 수 없습니다."); + } + this.value = value; + } + + int profit(final double profit) { + return (int) (value * profit); + } +} 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..eeb69643c9b --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,18 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.dto.CardResponse; + +public class Dealer extends Participant { + + private static final int CARD_DRAW_POINT = 16; + + @Override + public boolean isDrawable() { + final int currentScore = currentScore(); + return currentScore <= CARD_DRAW_POINT; + } + + public CardResponse getFirstCard() { + return getCards().get(0); + } +} diff --git a/src/main/java/blackjack/domain/participant/Name.java b/src/main/java/blackjack/domain/participant/Name.java new file mode 100644 index 00000000000..4c53b49bff0 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Name.java @@ -0,0 +1,34 @@ +package blackjack.domain.participant; + +public class Name { + + private static final int MAX_NAME_LENGTH = 100; + + private final String value; + + Name(final String value) { + validateName(value); + this.value = value; + } + + private void validateName(final String name) { + validateEmptyName(name); + validateLength(name); + } + + private void validateEmptyName(final String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("이름을 입력하지 않았습니다"); + } + } + + private void validateLength(final String name) { + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름이 " + MAX_NAME_LENGTH + "글자를 초과했습니다"); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/blackjack/domain/participant/Names.java b/src/main/java/blackjack/domain/participant/Names.java new file mode 100644 index 00000000000..845c143756a --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Names.java @@ -0,0 +1,40 @@ +package blackjack.domain.participant; + +import java.util.List; + +class Names { + + private static final int MAX_PLAYER_COUNT = 5; + private static final int MIN_PLAYER_COUNT = 1; + private static final String OVER_RANGE_MESSAGE = + "사용자 수는 " + MIN_PLAYER_COUNT + " 이상 " + MAX_PLAYER_COUNT + " 이하여야 합니다. 현재 : %s 명입니다"; + + Names(final List names) { + validatePlayerNames(names); + names.forEach(Name::new); + } + + private static void validatePlayerNames(final List playerNames) { + validateNull(playerNames); + validatePlayerCount(playerNames); + validateDuplicate(playerNames); + } + + private static void validateNull(final List playerNames) { + if (playerNames == null) { + throw new IllegalArgumentException("사용자 이름이 입력되지 않았습니다"); + } + } + + private static void validatePlayerCount(final List playerNames) { + if (MIN_PLAYER_COUNT > playerNames.size() || playerNames.size() > MAX_PLAYER_COUNT) { + throw new IllegalArgumentException(String.format(OVER_RANGE_MESSAGE, playerNames.size())); + } + } + + private static void validateDuplicate(final List playerNames) { + if (playerNames.stream().distinct().count() != playerNames.size()) { + throw new IllegalArgumentException("사용자의 이름이 중복됩니다."); + } + } +} diff --git a/src/main/java/blackjack/domain/participant/Participant.java b/src/main/java/blackjack/domain/participant/Participant.java new file mode 100644 index 00000000000..e7f5c916fac --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participant.java @@ -0,0 +1,39 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardPocket; +import blackjack.domain.card.Deck; +import blackjack.domain.card.dto.CardResponse; +import java.util.List; + +public abstract class Participant { + + private final CardPocket cardPocket; + + Participant() { + cardPocket = CardPocket.empty(); + } + + void drawInitialCard(final Deck deck) { + drawCard(deck.popCard()); + drawCard(deck.popCard()); + } + + public boolean hasBlackJack() { + return cardPocket.isBlackJack(); + } + + void drawCard(final Card card) { + cardPocket.addCard(card); + } + + public int currentScore() { + return cardPocket.calculateScore(); + } + + List getCards() { + return cardPocket.getCards(); + } + + public abstract boolean isDrawable(); +} 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..7e09e27a178 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participants.java @@ -0,0 +1,91 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.dto.CardResponse; +import java.util.List; +import java.util.Map; + +public class Participants { + + private final Players players; + private final Dealer dealer; + + private Participants(final Players players, final Dealer dealer) { + this.players = players; + this.dealer = dealer; + } + + public static Participants of(final List playerNames, final List bettingMoneys) { + return new Participants(Players.from(playerNames, bettingMoneys), new Dealer()); + } + + //이런 방식으로 검증을 하게 되면, 생성자에서 검증을 하기에, 로직상은 문제가 없지만 + //new Names().validate()와 같이 호출하는 것이 직관적일 수도 있을 것 같은데 어떤 방향이 좋으신가요? + //애그리거트 루트로 생각하다보니, 여기에 검증하는 작업을 넣었는데, 한번에 이름, 돈이 들어오지 않기에, 생성시 검증이 불가능합니다 + //그러다보니 static 으로 작성하게 되었습니다 + public static void validatePlayerNames(final List playerNames) { + new Names(playerNames); + } + + public static void validateBettingMoney(final int amount) { + new BettingMoney(amount); + } + + public void distributeInitialCards(final Deck deck) { + players.distributeInitialCards(deck); + dealer.drawInitialCard(deck); + } + + public boolean isPlayerDrawable(final String playerName) { + return players.isDrawable(playerName); + } + + public void drawPlayerCard(final String playerName, final Card card) { + players.draw(playerName, card); + } + + public boolean isDealerDrawable() { + return dealer.isDrawable(); + } + + public void drawDealerCard(final Card card) { + dealer.drawCard(card); + } + + public Dealer getDealer() { + return dealer; + } + + public List getPlayers() { + return players.getPlayers(); + } + + public List getPlayerNames() { + return players.getPlayerNames(); + } + + public CardResponse getDealerFirstCard() { + return dealer.getFirstCard(); + } + + public List getPlayerCards(final String playerName) { + return players.getPlayerCards(playerName); + } + + public Map> getPlayersCards() { + return players.getPlayersCards(); + } + + public int getDealerScore() { + return dealer.currentScore(); + } + + public List getDealerCards() { + return dealer.getCards(); + } + + public Map getPlayersScores() { + return players.calculatePlayersScore(); + } +} 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..f3a137180ac --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,31 @@ +package blackjack.domain.participant; + +public class Player extends Participant { + + private static final int BLACKJACK_SCORE = 21; + + private final PlayerInfo playerInfo; + + Player(final String name, final int bettingMoney) { + playerInfo = new PlayerInfo(name, bettingMoney); + } + + @Override + public boolean isDrawable() { + final int currentScore = currentScore(); + return currentScore < BLACKJACK_SCORE; + } + + public String getName() { + return playerInfo.getName().getValue(); + } + + boolean hasName(final String playerName) { + return playerInfo.getName().getValue() + .equals(playerName); + } + + public int calculateProfit(final double profit) { + return playerInfo.getBettingMoney().profit(profit); + } +} diff --git a/src/main/java/blackjack/domain/participant/PlayerInfo.java b/src/main/java/blackjack/domain/participant/PlayerInfo.java new file mode 100644 index 00000000000..8ad7c6426c6 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/PlayerInfo.java @@ -0,0 +1,20 @@ +package blackjack.domain.participant; + +public class PlayerInfo { + + private final Name name; + private final BettingMoney bettingMoney; + + public PlayerInfo(final String name, final int bettingMoney) { + this.name = new Name(name); + this.bettingMoney = new BettingMoney(bettingMoney); + } + + public Name getName() { + return name; + } + + public BettingMoney getBettingMoney() { + return bettingMoney; + } +} 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..f4330241327 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Players.java @@ -0,0 +1,103 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.dto.CardResponse; +import blackjack.domain.participant.exception.PlayerNotFoundException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class Players { + + private final List players; + + private Players(final List players) { + this.players = players; + } + + static Players from(final List playerNames, final List bettingMoneys) { + validatePlayerNames(playerNames); + validateSize(playerNames, bettingMoneys); + final List players = createPlayers(playerNames, bettingMoneys); + return new Players(players); + } + + private static void validateSize(final List playerNames, final List bettingMoneys) { + if (playerNames.size() != bettingMoneys.size()) { + throw new IllegalArgumentException("플레이어 이름과 베팅 금액의 수가 일치하지 않습니다."); + } + } + + private static List createPlayers(final List playerNames, final List bettingMoneys) { + final List players = new ArrayList<>(); + for (int i = 0; i < playerNames.size(); i++) { + players.add(new Player(playerNames.get(i), bettingMoneys.get(i))); + } + return players; + } + + private static void validatePlayerNames(final List playerNames) { + new Names(playerNames); + } + + void distributeInitialCards(final Deck deck) { + for (final Player player : players) { + player.drawInitialCard(deck); + } + } + + List getPlayerNames() { + return players.stream() + .map(Player::getName) + .collect(Collectors.toList()); + } + + boolean isDrawable(final String playerName) { + return players.stream() + .filter(player -> player.hasName(playerName)) + .findFirst() + .map(Player::isDrawable) + .orElseThrow(PlayerNotFoundException::new); + } + + void draw(final String playerName, final Card card) { + final Player targetPlayer = players.stream() + .filter(player -> player.hasName(playerName)) + .findFirst() + .orElseThrow(PlayerNotFoundException::new); + targetPlayer.drawCard(card); + } + + List getPlayers() { + return players; + } + + List getPlayerCards(final String playerName) { + return players.stream() + .filter(player -> player.hasName(playerName)) + .findFirst() + .map(Player::getCards) + .orElseThrow(PlayerNotFoundException::new); + } + + Map calculatePlayersScore() { + final Map playerScore = new LinkedHashMap<>(); + for (final Player player : players) { + playerScore.put(player.getName(), player.currentScore()); + } + return playerScore; + } + + Map> getPlayersCards() { + final Map> playerCards = new HashMap<>(); + for (final Player player : players) { + playerCards.put(player.getName(), + player.getCards()); + } + return playerCards; + } +} diff --git a/src/main/java/blackjack/domain/participant/exception/PlayerNotFoundException.java b/src/main/java/blackjack/domain/participant/exception/PlayerNotFoundException.java new file mode 100644 index 00000000000..376df990e4d --- /dev/null +++ b/src/main/java/blackjack/domain/participant/exception/PlayerNotFoundException.java @@ -0,0 +1,8 @@ +package blackjack.domain.participant.exception; + +public class PlayerNotFoundException extends RuntimeException { + + public PlayerNotFoundException() { + super("없는 사용자 입니다"); + } +} diff --git a/src/main/java/blackjack/view/DrawCommand.java b/src/main/java/blackjack/view/DrawCommand.java new file mode 100644 index 00000000000..cc369f743e9 --- /dev/null +++ b/src/main/java/blackjack/view/DrawCommand.java @@ -0,0 +1,20 @@ +package blackjack.view; + +import java.util.Arrays; + +public enum DrawCommand { + DRAW("y"), + STAY("n"); + public final String command; + + DrawCommand(final String command) { + this.command = command; + } + + public static DrawCommand from(final String command) { + return Arrays.stream(values()) + .filter(it -> it.command.equalsIgnoreCase(command)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("y, n 만 입력 가능합니다")); + } +} diff --git a/src/main/java/blackjack/view/IOView.java b/src/main/java/blackjack/view/IOView.java new file mode 100644 index 00000000000..a1cda051aa3 --- /dev/null +++ b/src/main/java/blackjack/view/IOView.java @@ -0,0 +1,60 @@ +package blackjack.view; + +import blackjack.domain.card.dto.CardResponse; +import java.util.List; +import java.util.Map; + +public class IOView { + + private final InputView inputView; + private final OutputView outputView; + + public IOView(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public List inputPlayerNames() { + return inputView.inputPlayerNames(); + } + + public DrawCommand inputCommand(final String playerName) { + return inputView.inputCommand(playerName); + } + + public int inputPlayerMoney(final String playerName) { + return inputView.inputPlayerMoney(playerName); + } + + public void printInitialCards( + final CardResponse dealerCard, + final Map> playerNameToCards) { + outputView.printInitialCards(dealerCard, playerNameToCards); + } + + public void printCardStatusOfPlayer(final String planerName, final List playerCardsResponse) { + outputView.printCardStatusOfPlayer(planerName, playerCardsResponse); + } + + public void printDealerCardDrawMessage() { + outputView.printDealerCardDrawMessage(); + } + + public void printFinalStatusOfDealer(final int score, final List dealerCards) { + outputView.printFinalStatusOfDealer(score, dealerCards); + } + + public void printFinalStatusOfPlayers( + final Map> playersCardsResponse, + final Map playersScore) { + outputView.printFinalStatusOfPlayers(playersCardsResponse, playersScore); + } + + public void printFinalMoney(final Map calculatePlayersMoney) { + outputView.printFinalMoney(calculatePlayersMoney); + } + + public void printError(final Exception e) { + System.err.println(e.getMessage()); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..f6e9ec0716c --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,43 @@ +package blackjack.view; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + private static final String INPUT_PLAYER_NAMES_MESSAGE = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"; + private static final String INPUT_COMMAND_MESSAGE = "{0}은/는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"; + private static final String INPUT_DELIMITER = ","; + + public List inputPlayerNames() { + System.out.println(INPUT_PLAYER_NAMES_MESSAGE); + final String input = scanner.nextLine(); + return Arrays.stream(input.split(INPUT_DELIMITER, -1)) + .map(String::strip) + .collect(Collectors.toList()); + } + + public DrawCommand inputCommand(final String playerName) { + System.out.println(MessageFormat.format(INPUT_COMMAND_MESSAGE, playerName)); + final String input = scanner.nextLine(); + return DrawCommand.from(input); + } + + public int inputPlayerMoney(final String playerName) { + System.out.println(MessageFormat.format("{0}의 배팅 금액은?", playerName)); + try { + return Integer.parseInt(scanner.nextLine()); + } catch (final NumberFormatException e) { + System.out.println("숫자만 입력해주세요."); + return inputPlayerMoney(playerName); + } + } + + public void printInputError(final Exception e) { + System.out.println(e.getMessage()); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000000..f44b1f08bcb --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,110 @@ +package blackjack.view; + +import blackjack.domain.card.dto.CardResponse; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String OUTPUT_DISTRIBUTE_MESSAGE = "딜러와 {0}에게 2장을 나누었습니다."; + + private static final String OUTPUT_DEALER_STATUS_MESSAGE = "딜러는 16이하라 한장의 카드를 더 받았습니다."; + private static final String DELIMITER_BETWEEN_CARDS = ", "; + private static final String DELIMITER = ": "; + private static final String DEALER = "딜러"; + private static final String CARD = "카드"; + private static final String RESULT = " - 결과: "; + + public void printInitialCards(final CardResponse dealerCard, + final Map> playerNameToCards) { + printInitialMessage(playerNameToCards); + printInitialDealerCard(dealerCard); + playerNameToCards.forEach(this::printInitialPlayerCard); + System.out.println(); + } + + private void printInitialMessage(final Map> playerNameToCards) { + final String playerNames = String.join(DELIMITER_BETWEEN_CARDS, playerNameToCards.keySet()); + System.out.println(); + System.out.println(MessageFormat.format(OUTPUT_DISTRIBUTE_MESSAGE, playerNames)); + } + + private void printInitialDealerCard(final CardResponse cardResponse) { + System.out.println(DEALER + DELIMITER + convertCard(cardResponse)); + } + + private void printInitialPlayerCard(final String name, final List cards) { + final String card = cards.stream() + .map(this::convertCard) + .collect(Collectors.joining(DELIMITER_BETWEEN_CARDS)); + + System.out.println(name + CARD + DELIMITER + card); + } + + private String convertCard(final CardResponse cardResponse) { + final String convertedSymbol = cardResponse.getSymbol(); + final String convertedShape = cardResponse.getShape(); + + return convertedSymbol + convertedShape; + } + + public void printCardStatusOfPlayer(final String planerName, final List playerCardsResponse) { + final String cards = playerCardsResponse + .stream() + .map(this::convertCard) + .collect(Collectors.joining(DELIMITER_BETWEEN_CARDS)); + System.out.println(planerName + DELIMITER + cards); + } + + public void printDealerCardDrawMessage() { + System.out.println(); + System.out.println(OUTPUT_DEALER_STATUS_MESSAGE); + } + + public void printFinalStatusOfDealer(final int score, final List dealerCards) { + final String cards = dealerCards + .stream() + .map(this::convertCard) + .collect(Collectors.joining(DELIMITER_BETWEEN_CARDS)); + System.out.println(); + System.out.println(DEALER + " " + CARD + DELIMITER + cards + RESULT + score); + } + + public void printFinalStatusOfPlayers(final Map> playersCardsResponse, + final Map playersScore) { + playersScore.keySet().forEach(name -> { + printFinalPlayerCard(name, playersCardsResponse.get(name)); + printFinalPlayerScore(playersScore.get(name)); + }); + } + + + private void printFinalPlayerCard(final String name, final List cards) { + final String card = cards.stream() + .map(this::convertCard) + .collect(Collectors.joining(DELIMITER_BETWEEN_CARDS)); + + System.out.print(name + CARD + DELIMITER + card); + } + + private void printFinalPlayerScore(final int score) { + System.out.println(RESULT + score); + } + + public void printFinalMoney(final Map calculatePlayersMoney) { + System.out.println(); + System.out.println("## 최종 수익"); + System.out.println(DEALER + DELIMITER + calculateDealerMoney(calculatePlayersMoney)); + calculatePlayersMoney.forEach((name, money) -> System.out.println(name + DELIMITER + money)); + } + + private String calculateDealerMoney(final Map calculatePlayersMoney) { + int dealerMoney = 0; + for (final int money : calculatePlayersMoney.values()) { + dealerMoney -= money; + } + return String.valueOf(dealerMoney); + } +} diff --git a/src/test/java/blackjack/domain/card/CardFixture.java b/src/test/java/blackjack/domain/card/CardFixture.java new file mode 100644 index 00000000000..df9cdf3e317 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardFixture.java @@ -0,0 +1,12 @@ +package blackjack.domain.card; + +public class CardFixture { + + public static final Card CLOVER_ACE = new Card(Shape.CLOVER, Symbol.ACE); + public static final Card SPADE_ACE = new Card(Shape.SPADE, Symbol.ACE); + public static final Card HEART_EIGHT = new Card(Shape.HEART, Symbol.EIGHT); + public static final Card DIAMOND_TEN = new Card(Shape.DIAMOND, Symbol.TEN); + public static final Card DIAMOND_EIGHT = new Card(Shape.DIAMOND, Symbol.EIGHT); + public static final Card CLOVER_TWO = new Card(Shape.CLOVER, Symbol.TWO); + public static final Card HEART_KING = new Card(Shape.HEART, Symbol.KING); +} diff --git a/src/test/java/blackjack/domain/card/CardPocketTest.java b/src/test/java/blackjack/domain/card/CardPocketTest.java new file mode 100644 index 00000000000..387a6013541 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardPocketTest.java @@ -0,0 +1,78 @@ +package blackjack.domain.card; + +import static blackjack.domain.card.CardFixture.CLOVER_ACE; +import static blackjack.domain.card.CardFixture.DIAMOND_EIGHT; +import static blackjack.domain.card.CardFixture.DIAMOND_TEN; +import static blackjack.domain.card.CardFixture.HEART_EIGHT; +import static blackjack.domain.card.CardFixture.SPADE_ACE; +import static blackjack.domain.card.CardResponseFixture.CLOVER_ACE_RESPONSE; +import static blackjack.domain.card.CardResponseFixture.HEART_EIGHT_RESPONSE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CardPocketTest { + + private static final List cards = List.of(CLOVER_ACE, HEART_EIGHT); + + private CardPocket cardPocket; + + @BeforeEach + void setCardPocket() { + cardPocket = CardPocket.empty(); + cards.forEach(cardPocket::addCard); + } + + @Test + void 카드_포켓_안의_카드의_점수_계산() { + assertThat(cardPocket.calculateScore()) + .isEqualTo(19); + + } + + @Test + void 카드_포켓에_카드_추가한_후_카드의_점수_계산() { + cardPocket.addCard(SPADE_ACE); + + assertThat(cardPocket.calculateScore()) + .isEqualTo(20); + + } + + @Test + void 카드_포켓에_카드_두번_추가한_후_카드의_점수_계산() { + cardPocket.addCard(DIAMOND_TEN); + cardPocket.addCard(DIAMOND_EIGHT); + + assertThat(cardPocket.calculateScore()) + .isEqualTo(27); + } + + @Test + void 카드_포켓에서_카드_가져오는_기능_추가() { + assertThat(cardPocket.getCards()) + .containsExactly(CLOVER_ACE_RESPONSE, HEART_EIGHT_RESPONSE); + } + + @ParameterizedTest + @CsvSource(value = {"ACE, TEN, true", "JACK, ACE, true", "ACE, ACE, false", "TEN, TEN, false"}) + void 블랙잭인_경우_2장으로_21점이_되는_경우_검증( + final Symbol firstSymbol, + final Symbol secondSymbol, + final boolean expected) { + final CardPocket cardPocket = CardPocket.empty(); + cardPocket.addCard(new Card(Shape.DIAMOND, firstSymbol)); + cardPocket.addCard(new Card(Shape.DIAMOND, secondSymbol)); + + assertThat(cardPocket.isBlackJack()) + .isEqualTo(expected); + } +} diff --git a/src/test/java/blackjack/domain/card/CardResponseFixture.java b/src/test/java/blackjack/domain/card/CardResponseFixture.java new file mode 100644 index 00000000000..08c4b69bd73 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardResponseFixture.java @@ -0,0 +1,13 @@ +package blackjack.domain.card; + +import blackjack.domain.card.dto.CardResponse; + +public class CardResponseFixture { + + public static final CardResponse SPADE_ACE_RESPONSE = CardResponse.from( + new Card(Shape.SPADE, Symbol.ACE)); + public static final CardResponse CLOVER_ACE_RESPONSE = CardResponse.from( + new Card(Shape.CLOVER, Symbol.ACE)); + static final CardResponse HEART_EIGHT_RESPONSE = CardResponse.from( + new Card(Shape.HEART, Symbol.EIGHT)); +} 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..9ff4636524e --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,41 @@ +package blackjack.domain.card; + +import static blackjack.domain.card.CardFixture.CLOVER_ACE; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class CardTest { + + private final Shape shape = Shape.CLOVER; + private final Symbol symbol = Symbol.ACE; + private final Card card = CLOVER_ACE; + + @Test + void 카드의_Symbol을_반환한다() { + assertThat(card.getSymbol()) + .isEqualTo(symbol); + } + + @Test + void 카드의_Shape를_반환한다() { + assertThat(card.getShape()) + .isEqualTo(shape); + } + + @Test + void 카드의_점수를_반환한다() { + assertThat(card.getScore()) + .isEqualTo(symbol.getScore()); + } + + @Test + void 카드가_Ace인지_확인한다() { + assertThat(card.isAce()) + .isEqualTo(symbol.isAce()); + } +} 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..9b2660d6815 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,38 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayDeque; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DeckTest { + + @Test + void 생성시_null_이면_예외() { + assertThatThrownBy(() -> new Deck(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("카드에 null 이 들어왔습니다"); + } + + @Test + void 생성시_52장이_아니면_예외() { + assertThatThrownBy(() -> new Deck(new ArrayDeque<>())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("카드 숫자는 52장이어야 합니다 현재 :0장"); + } + + @Test + void 제거_시도를_52번보다_많이_하면_예외() { + final Deck deck = new ShuffledDeckFactory().generate(); + for (int i = 0; i < 52; i++) { + deck.popCard(); + } + assertThatThrownBy(deck::popCard) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("덱에 카드가 없습니다"); + } +} diff --git a/src/test/java/blackjack/domain/card/NotShuffledDeckFactory.java b/src/test/java/blackjack/domain/card/NotShuffledDeckFactory.java new file mode 100644 index 00000000000..118af606819 --- /dev/null +++ b/src/test/java/blackjack/domain/card/NotShuffledDeckFactory.java @@ -0,0 +1,22 @@ +package blackjack.domain.card; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class NotShuffledDeckFactory implements DeckFactory { + + @Override + public Deck generate() { + final List cards = generateCards(); + return new Deck(new ArrayDeque<>(cards)); + } + + private List generateCards() { + return Arrays.stream(Shape.values()) + .flatMap(shape -> Arrays.stream(Symbol.values()) + .map(symbol -> new Card(shape, symbol))) + .collect(Collectors.toList()); + } +} diff --git a/src/test/java/blackjack/domain/participant/BettingMoneyTest.java b/src/test/java/blackjack/domain/participant/BettingMoneyTest.java new file mode 100644 index 00000000000..e8be3937ab6 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/BettingMoneyTest.java @@ -0,0 +1,28 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class BettingMoneyTest { + + @Test + void 음수_금액이_입력될_수_없습니댜() { + assertThatThrownBy(() -> new BettingMoney(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("베팅 금액은 0보다 작을 수 없습니다."); + } + + @Test + void profit() { + final BettingMoney bettingMoney = new BettingMoney(1000); + final int result = bettingMoney.profit(1); + assertThat(result) + .isEqualTo(1000); + } +} 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..3690a9cb95f --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,41 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DealerTest { + + private static final List overDrawPointCards = List.of( + new Card(Shape.CLOVER, Symbol.ACE), + new Card(Shape.HEART, Symbol.KING)); + private static final List underDrawPointCards = List.of( + new Card(Shape.CLOVER, Symbol.TWO), + new Card(Shape.HEART, Symbol.EIGHT)); + + @Test + void 딜러의_카드가_16_이하의_점수라면_드로우_합니다() { + final Dealer dealer = new Dealer(); + underDrawPointCards.forEach(dealer::drawCard); + + assertThat(dealer.isDrawable()) + .isTrue(); + } + + @Test + void 딜러의_카드가_17_이상의_점수라면_스테이_합니다() { + final Dealer dealer = new Dealer(); + overDrawPointCards.forEach(dealer::drawCard); + + assertThat(dealer.isDrawable()) + .isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/participant/NameTest.java b/src/test/java/blackjack/domain/participant/NameTest.java new file mode 100644 index 00000000000..d1242f67791 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/NameTest.java @@ -0,0 +1,31 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.IntStream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class NameTest { + + @Test + void 생성시_null이면_예외() { + assertThatThrownBy(() -> new Name(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름을 입력하지 않았습니다"); + } + + @Test + void 길이가_100글자_초과시_에러() { + final StringBuilder stringBuilder = new StringBuilder(); + IntStream.range(0, 100) + .forEach(stringBuilder::append); + + assertThatThrownBy(() -> new Name(stringBuilder.toString())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름이 100글자를 초과했습니다"); + } +} diff --git a/src/test/java/blackjack/domain/participant/NamesTest.java b/src/test/java/blackjack/domain/participant/NamesTest.java new file mode 100644 index 00000000000..2837149fcd0 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/NamesTest.java @@ -0,0 +1,48 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +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; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class NamesTest { + + private static Stream provideNames() { + return Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of("pobi", "crong", "honux", "wannte", "디디", "누누")) + ); + } + + @Test + void 이름이_목록이_null_이면_예외() { + assertThatThrownBy(() -> new Names(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자 이름이 입력되지 않았습니다"); + } + + @ParameterizedTest + @MethodSource("provideNames") + void 이름의_수가_0부터_5까지만_가능하다(final List playerNames) { + assertThatThrownBy(() -> new Names(playerNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자 수는 1 이상 5 이하여야 합니다. 현재 : " + playerNames.size() + " 명입니다"); + } + + @Test + void 이름이_이름이_중복되면_예외() { + final List playerNames = List.of("pobi", "pobi", "honux", "wannte", "디디"); + + assertThatThrownBy(() -> new Names(playerNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자의 이름이 중복됩니다."); + } +} diff --git a/src/test/java/blackjack/domain/participant/ParticipantTest.java b/src/test/java/blackjack/domain/participant/ParticipantTest.java new file mode 100644 index 00000000000..db12139e629 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/ParticipantTest.java @@ -0,0 +1,58 @@ +package blackjack.domain.participant; + +import static blackjack.domain.card.CardResponseFixture.CLOVER_ACE_RESPONSE; +import static blackjack.domain.card.CardResponseFixture.SPADE_ACE_RESPONSE; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.card.NotShuffledDeckFactory; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class ParticipantTest { + + private Participant participant; + + @BeforeEach + void setUp() { + participant = new Participant() { + @Override + public boolean isDrawable() { + return true; + } + }; + final NotShuffledDeckFactory deckFactory = new NotShuffledDeckFactory(); + final Deck deck = deckFactory.generate(); + participant.drawInitialCard(deck); + } + + @Test + void 참가자는_점수를_계산할_수_있다() { + + final int score = participant.currentScore(); + + assertThat(score).isEqualTo(12); + } + + @Test + void 참가자는_추가로_드로우를_해서_점수를_계산할_수_있다() { + participant.drawCard(new Card(Shape.CLOVER, Symbol.ACE)); + + final int score = participant.currentScore(); + + assertThat(score).isEqualTo(13); + } + + @Test + void 참가자는_자기가_가지고_있는_카드를_확인할_수_있다() { + assertThat(participant.getCards()) + .containsExactly(SPADE_ACE_RESPONSE, CLOVER_ACE_RESPONSE); + } +} 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..21a79deff32 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,53 @@ +package blackjack.domain.participant; + +import static blackjack.domain.card.CardFixture.CLOVER_ACE; +import static blackjack.domain.card.CardFixture.CLOVER_TWO; +import static blackjack.domain.card.CardFixture.HEART_EIGHT; +import static blackjack.domain.card.CardFixture.HEART_KING; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PlayerTest { + + private static final List overDrawPointCards = List.of( + CLOVER_ACE, + HEART_KING); + private static final List underDrawPointCards = List.of( + CLOVER_TWO, + HEART_EIGHT); + + private Player player; + + @BeforeEach + void setup() { + player = new Player("pobi", 10000); + } + + @Test + void 플레이어_카드의_점수가_21미만이면_드로우_할수있다() { + underDrawPointCards.forEach(player::drawCard); + assertThat(player.isDrawable()) + .isTrue(); + } + + @Test + void 플레이어_카드_점수가_21이상이면_드로우_할수없다() { + overDrawPointCards.forEach(player::drawCard); + assertThat(player.isDrawable()) + .isFalse(); + } + + @Test + void 플레이어는_베팅_금액을_가진다() { + assertThat(player.calculateProfit(1)) + .isEqualTo(10000); + } +} 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..64343a8258d --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayersTest.java @@ -0,0 +1,55 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PlayersTest { + + @Nested + @DisplayName("Players를 생성할 때") + class PlayerInitiatorTest { + + @Test + void 플레이어의_목록이_null_이면_예외() { + assertThatThrownBy(() -> Players.from(null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자 이름이 입력되지 않았습니다"); + } + + @Test + void 플레이어의_수가_0명이면_예외() { + final List playerNames = new ArrayList<>(); + + assertThatThrownBy(() -> Players.from(playerNames, List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자 수는 1 이상 5 이하여야 합니다. 현재 : 0 명입니다"); + } + + @Test + void 플레이어의_수가_5명초과면_예외() { + final List playerNames = List.of("pobi", "crong", "honux", "wannte", "디디", "누누"); + + assertThatThrownBy(() -> Players.from(playerNames, List.of(1, 2, 3, 4, 5, 6))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자 수는 1 이상 5 이하여야 합니다. 현재 : 6 명입니다"); + } + + @Test + void 플레이어의_이름이_중복되면_예외() { + final List playerNames = List.of("pobi", "pobi", "honux", "wannte", "디디"); + + assertThatThrownBy(() -> Players.from(playerNames, List.of(1, 2, 3, 4, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("사용자의 이름이 중복됩니다."); + } + } +}