diff --git a/README.md b/README.md index 556099c4de..ccdaf59ccd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,68 @@ 블랙잭 미션 저장소 -## 우아한테크코스 코드리뷰 +## 페어와 지킬 컨벤션 +1. 클래스 정의 다음 줄은 공백으로 한다. +2. test code에 사용하는 메서드는 `static import`한다. -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +## 요구사항 +### 카드 +- [x] 카드는 모양과 수로 이루어져 있다. + - [x] 모양으로는 스페이드, 다이아, 하트, 클로버가 있다. + - [x] 수로는 Ace, King, Queen, Jack과 2 이상 10 이하의 정수가 있다. +- [x] 카드의 숫자 계산은 카드 숫자를 기본으로 한다. + - [x] Ace는 1 또는 11로 계산할 수 있다. + - [x] King, Queen, Jack은 각각 10으로 계산한다. + +### 덱 +- [x] 맨 위의 카드 한 장을 뽑는다. +- [x] 덱에 카드가 없는 경우, 예외를 발생한다. + +### 플레이어 +- [x] 덱에서 카드를 한 장 뽑는다. +- [x] 지금까지 뽑은 카드의 점수를 계산한다. + +### 이름 +- [x] 이름은 2글자 이상 5글자 이하이다. +- [x] 이름은 공백으로만 구성될 수 없다. + +### 플레이어들 +- [x] 플레이어들의 이름은 중복될 수 없다. +- [x] 플레이어들의 인원수는 1명 이상 10명 이하이다. + +### 베팅 결과 +- [x] 플레이어별 배팅 금액을 저장한다. + +### 배팅 금액 +- [x] 배팅 금액은 자연수이다. + +### 딜러 +- [x] 플레이어의 기능을 상속한다. +- [x] 17점 이상이 될 때까지 카드를 계속 뽑는다. + +### 게임 결과 +- [x] 게임 결과를 아래의 순서대로 올바르게 판단한다. + 1. 플레이어는 배팅 금액의 1.5배만큼 돈을 번다. + - [x] 플레이어가 블랙잭이고 딜러가 블랙잭이 아닌 경우 + 2. 플레이어는 배팅 금액만큼 돈을 번다. + - [x] 플레이어가 버스트가 아니고 딜러가 버스트인 경우 + - [x] 플레이어와 딜러가 버스트가 아니고 플레이어의 점수가 더 큰 경우 + 3. 플레이어는 배팅 금액을 잃는다. + - [x] 플레이어가 버스트인 경우 + - [x] 플레이어와 딜러가 버스트가 아니고 딜러의 점수가 더 큰 경우 + - [x] 플레이어가 블랙잭이 아니고 딜러가 블랙잭인 경우 + 4. 플레이어는 배팅 금액을 다시 돌려받는다. + - [x] 플레이어와 딜러가 버스트가 아니고 동점인 경우 +- [x] 각 플레이어별 게임 결과를 저장한다. + +--- + +### 게임 흐름 +- [x] (hit, stand) 21을 넘지 않을 경우 원한다면 카드를 계속 뽑을 수 있다. +- [x] 게임이 시작할 때 딜러와 플레이어는 두 장의 카드를 지급 받는다. +- [x] 플레이어의 카드는 모두에게 공개된다. +- [x] 딜러의 두 번째 카드는 공개되지 않는다. +- [x] 플레이어가 카드를 새로 뽑을 때마다 카드 현황을 공개한다. +- [x] 각 플레이어는 딜러와만 승패를 겨룬다. +- [x] 딜러는 모든 플레이어와 승패를 겨룬다. +- [x] 게임을 완료한 후 딜러를 포함한 모든 플레이어의 수익을 확인한다. diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/java/blackjack/BlackJackMain.java b/src/main/java/blackjack/BlackJackMain.java new file mode 100644 index 0000000000..9be8abc104 --- /dev/null +++ b/src/main/java/blackjack/BlackJackMain.java @@ -0,0 +1,19 @@ +package blackjack; + +import blackjack.game.BlackJackGame; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class BlackJackMain { + + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + BlackJackGame blackJackGame = new BlackJackGame(inputView, outputView); + try { + blackJackGame.play(); + } catch (Exception e) { + outputView.printExceptionMessage(e); + } + } +} diff --git a/src/main/java/blackjack/card/Card.java b/src/main/java/blackjack/card/Card.java new file mode 100644 index 0000000000..0c0992c14b --- /dev/null +++ b/src/main/java/blackjack/card/Card.java @@ -0,0 +1,48 @@ +package blackjack.card; + +import blackjack.player.Score; +import java.util.Objects; + +public class Card { + + private final Shape shape; + private final Rank rank; + + public Card(Shape shape, Rank rank) { + this.shape = shape; + this.rank = rank; + } + + public boolean isAce() { + return rank == Rank.ACE; + } + + public Score getScore() { + return rank.getScore(); + } + + public Shape getShape() { + return shape; + } + + public Rank getRank() { + return rank; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Card card = (Card) o; + return shape == card.shape && rank == card.rank; + } + + @Override + public int hashCode() { + return Objects.hash(shape, rank); + } +} diff --git a/src/main/java/blackjack/card/Deck.java b/src/main/java/blackjack/card/Deck.java new file mode 100644 index 0000000000..9ed021cc26 --- /dev/null +++ b/src/main/java/blackjack/card/Deck.java @@ -0,0 +1,44 @@ +package blackjack.card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class Deck { + + private final Queue cards; + + Deck(List cards) { + this.cards = new LinkedList<>(cards); + } + + public static Deck createShuffledFullDeck() { + List cards = new LinkedList<>(); + for (Shape shape : Shape.values()) { + cards.addAll(createNumberCardsOf(shape)); + } + Collections.shuffle(cards); + return new Deck(cards); + } + + static List createNumberCardsOf(Shape shape) { + List cards = new ArrayList<>(); + for (Rank rank : Rank.values()) { + cards.add(new Card(shape, rank)); + } + return cards; + } + + public Card draw() { + if (cards.isEmpty()) { + throw new IllegalStateException("[ERROR] 덱이 비어있습니다."); + } + return cards.poll(); + } + + int size() { + return cards.size(); + } +} diff --git a/src/main/java/blackjack/card/Rank.java b/src/main/java/blackjack/card/Rank.java new file mode 100644 index 0000000000..826c73b8cd --- /dev/null +++ b/src/main/java/blackjack/card/Rank.java @@ -0,0 +1,30 @@ +package blackjack.card; + +import blackjack.player.Score; + +public enum Rank { + + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10), + QUEEN(10), + KING(10); + + private final Score score; + + Rank(int score) { + this.score = new Score(score); + } + + Score getScore() { + return this.score; + } +} diff --git a/src/main/java/blackjack/card/Shape.java b/src/main/java/blackjack/card/Shape.java new file mode 100644 index 0000000000..4ebd32ecc6 --- /dev/null +++ b/src/main/java/blackjack/card/Shape.java @@ -0,0 +1,9 @@ +package blackjack.card; + +public enum Shape { + + HEART, + SPADE, + CLOVER, + DIAMOND +} diff --git a/src/main/java/blackjack/game/BlackJackGame.java b/src/main/java/blackjack/game/BlackJackGame.java new file mode 100644 index 0000000000..2522a90930 --- /dev/null +++ b/src/main/java/blackjack/game/BlackJackGame.java @@ -0,0 +1,136 @@ +package blackjack.game; + +import blackjack.card.Deck; +import blackjack.player.Dealer; +import blackjack.player.Player; +import blackjack.player.Players; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.List; + +public class BlackJackGame { + + private final InputView inputView; + private final OutputView outputView; + + public BlackJackGame(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void play() { + Deck deck = Deck.createShuffledFullDeck(); + Dealer dealer = new Dealer(); + Players players = createPlayers(); + PlayerBettingMoney playerBettingMoney = decideBettingMoney(players); + initializeGame(deck, dealer, players); + proceedPlayersTurn(deck, players); + proceedDealerTurn(deck, dealer); + + showCardsWithScore(dealer, players); + showMatchResult(dealer, players, playerBettingMoney); + } + + private Players createPlayers() { + outputView.printNamesRequest(); + List names = inputView.readNames(); + Players players = new Players(names); + outputView.printNewLine(); + return players; + } + + private PlayerBettingMoney decideBettingMoney(Players players) { + PlayerBettingMoney playerBettingMoney = new PlayerBettingMoney(); + for (Player player : players.getPlayers()) { + outputView.printBettingRequestMessage(player.getName()); + Money money = new Money(inputView.readBattingAmount()); + playerBettingMoney.addBetting(player, money); + outputView.printNewLine(); + } + return playerBettingMoney; + } + + private void initializeGame(Deck deck, Dealer dealer, Players players) { + players.doInitialDraw(deck); + dealer.doInitialDraw(deck); + outputView.printInitializeBlackJack(players.getNames()); + showInitialCard(dealer, players); + } + + private void showInitialCard(Dealer dealer, Players players) { + outputView.printDealerFirstCard(dealer.getFirstCard()); + + for (Player player : players.getPlayers()) { + outputView.printPlayerCards(player.getName(), player.getCards()); + } + outputView.printNewLine(); + } + + private void proceedPlayersTurn(Deck deck, Players players) { + for (Player player : players.getPlayers()) { + proceedPlayerTurn(deck, player); + } + outputView.printNewLine(); + } + + private void proceedPlayerTurn(Deck deck, Player player) { + Command command = askPlayerToDrawMore(player); + if (command.isNo()) { + return; + } + player.drawCard(deck); + outputView.printPlayerCards(player.getName(), player.getCards()); + + if (player.hasDrawableScore()) { + proceedPlayerTurn(deck, player); + } + } + + private Command askPlayerToDrawMore(Player player) { + outputView.printDrawMoreCardRequest(player.getName()); + String input = inputView.readCommand(); + return Command.from(input); + } + + private void proceedDealerTurn(Deck deck, Dealer dealer) { + while (dealer.hasDrawableScore()) { + dealer.drawCard(deck); + outputView.printDealerDrawCard(); + outputView.printNewLine(); + } + } + + private void showCardsWithScore(Dealer dealer, Players players) { + outputView.printDealerCardsWithScore(dealer.getCards(), dealer.getScore()); + for (Player player : players.getPlayers()) { + outputView.printPlayerCardsWithScore(player.getName(), player.getCards(), player.getScore()); + } + outputView.printNewLine(); + } + + private void showMatchResult(Dealer dealer, Players players, PlayerBettingMoney bettingResults) { + MatchResults matchResults = calculateMatchResults(dealer, players, bettingResults); + outputView.printResultStart(); + showDealerResult(matchResults); + showPlayersResult(players, matchResults); + } + + private MatchResults calculateMatchResults(Dealer dealer, Players players, PlayerBettingMoney bettingResults) { + MatchResults matchResults = new MatchResults(dealer.getHand()); + for (Player player : players.getPlayers()) { + matchResults.addResult(player, bettingResults.getBettingAmountOf(player)); + } + return matchResults; + } + + private void showDealerResult(MatchResults matchResults) { + outputView.printDealerResult(matchResults.getDealerResult()); + } + + private void showPlayersResult(Players players, MatchResults matchResults) { + for (Player player : players.getPlayers()) { + int playerResult = matchResults.getResultOf(player); + outputView.printPlayerResult(player.getName(), playerResult); + } + } +} diff --git a/src/main/java/blackjack/game/Command.java b/src/main/java/blackjack/game/Command.java new file mode 100644 index 0000000000..1dcece9cbf --- /dev/null +++ b/src/main/java/blackjack/game/Command.java @@ -0,0 +1,26 @@ +package blackjack.game; + +import java.util.Arrays; +import java.util.Objects; + +public enum Command { + YES("y"), + NO("n"); + + private final String value; + + Command(String value) { + this.value = value; + } + + public static Command from(String value) { + return Arrays.stream(values()) + .filter(command -> command.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 명령어입니다.")); + } + + public boolean isNo() { + return Objects.equals(value, Command.NO.value); + } +} diff --git a/src/main/java/blackjack/game/MatchResult.java b/src/main/java/blackjack/game/MatchResult.java new file mode 100644 index 0000000000..885eaed4ca --- /dev/null +++ b/src/main/java/blackjack/game/MatchResult.java @@ -0,0 +1,62 @@ +package blackjack.game; + +import blackjack.player.Hand; +import blackjack.player.Score; + +public enum MatchResult { + + DEALER_WIN(-1), + PLAYER_WIN(1), + PLAYER_BLACKJACK(1.5), + TIE(0); + + private final double rateOfPrize; + + MatchResult(double rateOfPrize) { + this.rateOfPrize = rateOfPrize; + } + + public static double calculateRateOfPrize(Hand playerHand, Hand dealerHand) { + if (isPlayerBlackJackCondition(playerHand, dealerHand)) { + return PLAYER_BLACKJACK.rateOfPrize; + } + if (isPlayerWinningCondition(playerHand, dealerHand)) { + return PLAYER_WIN.rateOfPrize; + } + if (isDealerWinningCondition(playerHand, dealerHand)) { + return DEALER_WIN.rateOfPrize; + } + return TIE.rateOfPrize; + } + + private static boolean isPlayerBlackJackCondition(Hand playerHand, Hand dealerHand) { + return playerHand.isBlackJack() && !dealerHand.isBlackJack(); + } + + private static boolean isPlayerWinningCondition(Hand playerHand, Hand dealerHand) { + Score playerScore = playerHand.calculateScore(); + Score dealerScore = dealerHand.calculateScore(); + + if (playerScore.isBust()) { + return false; + } + return dealerScore.isBust() || playerScore.isLargerThan(dealerScore); + } + + private static boolean isDealerWinningCondition(Hand playerHand, Hand dealerHand) { + Score playerScore = playerHand.calculateScore(); + Score dealerScore = dealerHand.calculateScore(); + + if (playerScore.isBust()) { + return true; + } + if (dealerHand.isBlackJack() && !playerHand.isBlackJack()) { + return true; + } + return dealerScore.isNotBust() && dealerScore.isLargerThan(playerScore); + } + + double getRateOfPrize() { + return rateOfPrize; + } +} diff --git a/src/main/java/blackjack/game/MatchResults.java b/src/main/java/blackjack/game/MatchResults.java new file mode 100644 index 0000000000..c894aaaab8 --- /dev/null +++ b/src/main/java/blackjack/game/MatchResults.java @@ -0,0 +1,38 @@ +package blackjack.game; + +import blackjack.player.Hand; +import blackjack.player.Player; +import java.util.HashMap; +import java.util.Map; + +public class MatchResults { + + private final Hand dealerHand; + private final Map results; + + public MatchResults(Hand dealerHand) { + this.dealerHand = dealerHand; + this.results = new HashMap<>(); + } + + public void addResult(Player player, Money money) { + double rateOfPrize = MatchResult.calculateRateOfPrize(player.getHand(), dealerHand); + int prizeMoney = (int) (money.getMoney() * rateOfPrize); + results.put(player, prizeMoney); + } + + public int getResultOf(Player player) { + if (!results.containsKey(player)) { + throw new IllegalArgumentException("[ERROR] 존재하지 않는 플레이어입니다."); + } + return results.get(player); + } + + public int getDealerResult() { + int dealerResult = 0; + for (Map.Entry result : results.entrySet()) { + dealerResult -= result.getValue(); + } + return dealerResult; + } +} diff --git a/src/main/java/blackjack/game/Money.java b/src/main/java/blackjack/game/Money.java new file mode 100644 index 0000000000..2a75d96dde --- /dev/null +++ b/src/main/java/blackjack/game/Money.java @@ -0,0 +1,21 @@ +package blackjack.game; + +public class Money { + + private final int money; + + Money(int money) { + validateNatural(money); + this.money = money; + } + + private void validateNatural(int input) { + if (input <= 0) { + throw new IllegalArgumentException("[ERROR] 금액은 양수여야 합니다."); + } + } + + public int getMoney() { + return money; + } +} diff --git a/src/main/java/blackjack/game/PlayerBettingMoney.java b/src/main/java/blackjack/game/PlayerBettingMoney.java new file mode 100644 index 0000000000..5648d6ad45 --- /dev/null +++ b/src/main/java/blackjack/game/PlayerBettingMoney.java @@ -0,0 +1,28 @@ +package blackjack.game; + +import blackjack.player.Player; +import java.util.HashMap; +import java.util.Map; + +public class PlayerBettingMoney { + + private final Map bettingMoney; + + public PlayerBettingMoney() { + this.bettingMoney = new HashMap<>(); + } + + public void addBetting(Player player, Money money) { + if (bettingMoney.containsKey(player)) { + throw new IllegalArgumentException("[ERROR] 이미 존재하는 플레이어입니다."); + } + bettingMoney.put(player, money); + } + + public Money getBettingAmountOf(Player player) { + if (!bettingMoney.containsKey(player)) { + throw new IllegalArgumentException("[ERROR] 존재하지 않는 플레이어입니다."); + } + return bettingMoney.get(player); + } +} diff --git a/src/main/java/blackjack/player/Dealer.java b/src/main/java/blackjack/player/Dealer.java new file mode 100644 index 0000000000..d713c71656 --- /dev/null +++ b/src/main/java/blackjack/player/Dealer.java @@ -0,0 +1,30 @@ +package blackjack.player; + +import blackjack.card.Card; +import java.util.List; + +public class Dealer extends Player { + + private static final int MAX_DRAWABLE_SCORE = 16; + + public Dealer() { + super("딜러"); + } + + Dealer(Hand hand) { + super("딜러", hand); + } + + public Card getFirstCard() { + List cards = getCards(); + if (cards.isEmpty()) { + throw new IllegalStateException("[ERROR] 딜러가 카드를 가지고 있지 않습니다."); + } + return cards.get(0); + } + + @Override + public boolean hasDrawableScore() { + return getScore().isSmallerThanOrEqualTo(new Score(MAX_DRAWABLE_SCORE)); + } +} diff --git a/src/main/java/blackjack/player/Hand.java b/src/main/java/blackjack/player/Hand.java new file mode 100644 index 0000000000..f9ab63282f --- /dev/null +++ b/src/main/java/blackjack/player/Hand.java @@ -0,0 +1,52 @@ +package blackjack.player; + +import blackjack.card.Card; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Hand { + + private static final int BLACKJACK_COUNT = 2; + + private final List cards; + + public Hand(List cards) { + this.cards = cards; + } + + Hand() { + this(new ArrayList<>()); + } + + public Score calculateScore() { + Score minimumScore = calculateMinimumScore(); + if (!hasAce()) { + return minimumScore; + } + return minimumScore.changeToLargeAceScore(); + } + + private Score calculateMinimumScore() { + return cards.stream() + .map(Card::getScore) + .reduce(new Score(0), Score::add); + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + public void addCard(Card card) { + cards.add(card); + } + + public boolean isBlackJack() { + return calculateScore().isBlackJack() && getCards().size() == BLACKJACK_COUNT; + } + + public List getCards() { + return Collections.unmodifiableList(cards); + } +} diff --git a/src/main/java/blackjack/player/Name.java b/src/main/java/blackjack/player/Name.java new file mode 100644 index 0000000000..93a4bf79fe --- /dev/null +++ b/src/main/java/blackjack/player/Name.java @@ -0,0 +1,37 @@ +package blackjack.player; + +public class Name { + + private static final int MIN_NAME_LENGTH = 2; + private static final int MAX_NAME_LENGTH = 5; + + private final String name; + + Name(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + validateNonBlankName(name); + validateNameLength(name); + } + + private void validateNameLength(String name) { + if (name.length() < MIN_NAME_LENGTH || name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException( + "[ERROR] 이름은 " + MIN_NAME_LENGTH + "글자 이상 " + MAX_NAME_LENGTH + "글자 이하여야 합니다." + ); + } + } + + private void validateNonBlankName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("[ERROR] 이름은 공백일 수 없습니다."); + } + } + + String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/player/Player.java b/src/main/java/blackjack/player/Player.java new file mode 100644 index 0000000000..9bc68a1d91 --- /dev/null +++ b/src/main/java/blackjack/player/Player.java @@ -0,0 +1,49 @@ +package blackjack.player; + +import blackjack.card.Card; +import blackjack.card.Deck; +import java.util.List; + +public class Player { + + private final Name name; + private final Hand hand; + + public Player(String name, Hand hand) { + this.name = new Name(name); + this.hand = hand; + } + + public Player(String name) { + this(name, new Hand()); + } + + public void drawCard(Deck deck) { + hand.addCard(deck.draw()); + } + + public void doInitialDraw(Deck deck) { + drawCard(deck); + drawCard(deck); + } + + public boolean hasDrawableScore() { + return hand.calculateScore().isNotBust(); + } + + public String getName() { + return name.getName(); + } + + public Score getScore() { + return hand.calculateScore(); + } + + public List getCards() { + return hand.getCards(); + } + + public Hand getHand() { + return hand; + } +} diff --git a/src/main/java/blackjack/player/Players.java b/src/main/java/blackjack/player/Players.java new file mode 100644 index 0000000000..6c31cd41cf --- /dev/null +++ b/src/main/java/blackjack/player/Players.java @@ -0,0 +1,60 @@ +package blackjack.player; + +import blackjack.card.Deck; +import java.util.HashSet; +import java.util.List; + +public class Players { + + private static final int MIN_PLAYER_COUNT = 1; + private static final int MAX_PLAYER_COUNT = 10; + + private final List players; + + public Players(List playerNames) { + validatePlayers(playerNames); + this.players = playerNames.stream() + .map(Player::new) + .toList(); + } + + private void validatePlayers(List playerNames) { + validateNotNull(playerNames); + validateSize(playerNames); + validateUniqueNames(playerNames); + } + + private void validateNotNull(List playerNames) { + if (playerNames == null) { + throw new IllegalArgumentException("[ERROR] 플레이어로 null이 전달되었습니다."); + } + } + + private void validateSize(List playerNames) { + if (playerNames.size() < MIN_PLAYER_COUNT || playerNames.size() > MAX_PLAYER_COUNT) { + throw new IllegalArgumentException( + "[ERROR] 플레이어의 수는 " + MIN_PLAYER_COUNT + "명 이상 " + MAX_PLAYER_COUNT + "명 이하여야 합니다." + ); + } + } + + private void validateUniqueNames(List playerNames) { + if (new HashSet<>(playerNames).size() != playerNames.size()) { + throw new IllegalArgumentException("[ERROR] 이름은 중복될 수 없습니다."); + } + } + + public void doInitialDraw(Deck deck) { + players.forEach(player -> player.doInitialDraw(deck)); + } + + public List getNames() { + return players.stream() + .map(Player::getName) + .toList(); + } + + public List getPlayers() { + return players; + } +} diff --git a/src/main/java/blackjack/player/Score.java b/src/main/java/blackjack/player/Score.java new file mode 100644 index 0000000000..ad58843a41 --- /dev/null +++ b/src/main/java/blackjack/player/Score.java @@ -0,0 +1,72 @@ +package blackjack.player; + +import java.util.Objects; + +public class Score { + + private static final int BLACKJACK = 21; + private static final int ADDITIONAL_ACE = 10; + + private final int score; + + public Score(int score) { + this.score = score; + } + + public Score add(int score) { + return add(new Score(score)); + } + + public Score add(Score other) { + return new Score(this.score + other.score); + } + + public boolean isBust() { + return score > BLACKJACK; + } + + public boolean isNotBust() { + return !isBust(); + } + + public boolean isLargerThan(Score other) { + return this.score > other.score; + } + + public boolean isSmallerThanOrEqualTo(Score other) { + return !isLargerThan(other); + } + + public Score changeToLargeAceScore() { + Score largeAceScore = add(ADDITIONAL_ACE); + if (largeAceScore.isBust()) { + return this; + } + return largeAceScore; + } + + public boolean isBlackJack() { + return score == BLACKJACK; + } + + public int getScore() { + return score; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Score score1 = (Score) o; + return score == score1.score; + } + + @Override + public int hashCode() { + return Objects.hash(score); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..a13eeedf1c --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,36 @@ +package blackjack.view; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private static final String NAME_DELIMITER = ","; + + private final Scanner scanner = new Scanner(System.in); + + public List readNames() { + String input = scanner.nextLine(); + String[] names = input.split(NAME_DELIMITER); + return Arrays.stream(names).toList(); + } + + public int readBattingAmount() { + String input = scanner.nextLine(); + validateInteger(input); + return Integer.parseInt(input); + } + + private void validateInteger(String input) { + try { + Integer.parseInt(input); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("[ERROR] 정수가 아닙니다."); + } + } + + public String readCommand() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 0000000000..50bdc0ccda --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,82 @@ +package blackjack.view; + +import blackjack.card.Card; +import blackjack.player.Score; +import blackjack.view.display.CardRankDisplay; +import blackjack.view.display.CardShapeDisplay; +import java.util.List; + +public class OutputView { + + private static final String NAME_SEPARATOR = ", "; + private static final String CARD_SEPARATOR = ", "; + + public void printNamesRequest() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + } + + public void printBettingRequestMessage(String name) { + System.out.println(name + "의 배팅 금액은?"); + } + + public void printInitializeBlackJack(List names) { + System.out.println("딜러와 " + String.join(NAME_SEPARATOR, names) + "에게 2장을 나누었습니다."); + } + + public void printDealerFirstCard(Card card) { + System.out.println("딜러: " + convertCard(card)); + } + + public void printPlayerCards(String name, List cards) { + System.out.println(name + "카드: " + convertCards(cards)); + } + + public void printDrawMoreCardRequest(String name) { + System.out.println(name + "은(는) 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"); + } + + public void printDealerDrawCard() { + System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다."); + } + + public void printDealerCardsWithScore(List cards, Score score) { + printPlayerCardsWithScore("딜러", cards, score); + } + + public void printPlayerCardsWithScore(String name, List cards, Score score) { + System.out.println(name + " 카드: " + convertCards(cards) + " - 결과: " + score.getScore() + "점"); + } + + public void printResultStart() { + System.out.println("## 최종 수익"); + } + + public void printDealerResult(int dealerResult) { + System.out.println("딜러: " + dealerResult + "원"); + } + + public void printPlayerResult(String name, int result) { + System.out.println(name + ": " + result + "원"); + } + + public void printNewLine() { + System.out.println(); + } + + private String convertCard(Card card) { + String convertedRank = CardRankDisplay.getDisplayByRank(card.getRank()).getDisplay(); + String convertedShape = CardShapeDisplay.getDisplayByShape(card.getShape()).getDisplay(); + return convertedRank + convertedShape; + } + + private String convertCards(List cards) { + List convertedCards = cards.stream() + .map(this::convertCard) + .toList(); + return String.join(CARD_SEPARATOR, convertedCards); + } + + public void printExceptionMessage(Exception e) { + System.out.println(e.getMessage()); + } +} diff --git a/src/main/java/blackjack/view/display/CardRankDisplay.java b/src/main/java/blackjack/view/display/CardRankDisplay.java new file mode 100644 index 0000000000..e23a8c0638 --- /dev/null +++ b/src/main/java/blackjack/view/display/CardRankDisplay.java @@ -0,0 +1,40 @@ +package blackjack.view.display; + +import blackjack.card.Rank; +import java.util.Arrays; + +public enum CardRankDisplay { + + ACE(Rank.ACE, "A"), + TWO(Rank.TWO, "2"), + THREE(Rank.THREE, "3"), + FOUR(Rank.FOUR, "4"), + FIVE(Rank.FIVE, "5"), + SIX(Rank.SIX, "6"), + SEVEN(Rank.SEVEN, "7"), + EIGHT(Rank.EIGHT, "8"), + NINE(Rank.NINE, "9"), + TEN(Rank.TEN, "10"), + JACK(Rank.JACK, "J"), + QUEEN(Rank.QUEEN, "Q"), + KING(Rank.KING, "K"); + + private final Rank rank; + private final String display; + + CardRankDisplay(Rank rank, String display) { + this.rank = rank; + this.display = display; + } + + public static CardRankDisplay getDisplayByRank(Rank rank) { + return Arrays.stream(CardRankDisplay.values()) + .filter(displayNumber -> displayNumber.rank == rank) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 수입니다.")); + } + + public String getDisplay() { + return display; + } +} diff --git a/src/main/java/blackjack/view/display/CardShapeDisplay.java b/src/main/java/blackjack/view/display/CardShapeDisplay.java new file mode 100644 index 0000000000..91226a4223 --- /dev/null +++ b/src/main/java/blackjack/view/display/CardShapeDisplay.java @@ -0,0 +1,31 @@ +package blackjack.view.display; + +import blackjack.card.Shape; +import java.util.Arrays; + +public enum CardShapeDisplay { + + HEART(Shape.HEART, "하트"), + SPADE(Shape.SPADE, "스페이드"), + CLOVER(Shape.CLOVER, "클로버"), + DIAMOND(Shape.DIAMOND, "다이아몬드"); + + private final Shape shape; + private final String display; + + CardShapeDisplay(Shape shape, String display) { + this.shape = shape; + this.display = display; + } + + public static CardShapeDisplay getDisplayByShape(Shape shape) { + return Arrays.stream(CardShapeDisplay.values()) + .filter(displayShape -> displayShape.shape == shape) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 모양입니다.")); + } + + public String getDisplay() { + return display; + } +} diff --git a/src/main/java/blackjack/view/display/PlayerResultDisplay.java b/src/main/java/blackjack/view/display/PlayerResultDisplay.java new file mode 100644 index 0000000000..ba320eb162 --- /dev/null +++ b/src/main/java/blackjack/view/display/PlayerResultDisplay.java @@ -0,0 +1,29 @@ +package blackjack.view.display; + +import blackjack.game.MatchResult; +import java.util.Arrays; + +public enum PlayerResultDisplay { + PLAYER_WIN(MatchResult.PLAYER_WIN, "승"), + PLAYER_LOSE(MatchResult.DEALER_WIN, "패"), + TIE(MatchResult.TIE, "무"); + + private final MatchResult result; + private final String display; + + PlayerResultDisplay(MatchResult result, String display) { + this.result = result; + this.display = display; + } + + public static PlayerResultDisplay getDisplayByResult(MatchResult result) { + return Arrays.stream(values()) + .filter(displayResult -> displayResult.result == result) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("[ERROR] 존재하지 않는 결과입니다.")); + } + + public String getDisplay() { + return display; + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/java/blackjack/card/CardTest.java b/src/test/java/blackjack/card/CardTest.java new file mode 100644 index 0000000000..eca88ade62 --- /dev/null +++ b/src/test/java/blackjack/card/CardTest.java @@ -0,0 +1,30 @@ +package blackjack.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.player.Score; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardTest { + + @Test + @DisplayName("카드의 점수를 정확하게 계산한다.") + void calculateCardTest() { + Card card = new Card(Shape.SPADE, Rank.ACE); + assertThat(card.getScore()).isEqualTo(new Score(1)); + } + + @Test + @DisplayName("카드가 에이스인지 확인한다.") + void isAceTest() { + Card aceCard = new Card(Shape.HEART, Rank.ACE); + Card nonAceCard = new Card(Shape.CLOVER, Rank.JACK); + + assertAll( + () -> assertThat(aceCard.isAce()).isTrue(), + () -> assertThat(nonAceCard.isAce()).isFalse() + ); + } +} diff --git a/src/test/java/blackjack/card/DeckTest.java b/src/test/java/blackjack/card/DeckTest.java new file mode 100644 index 0000000000..0ff6dab591 --- /dev/null +++ b/src/test/java/blackjack/card/DeckTest.java @@ -0,0 +1,51 @@ +package blackjack.card; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DeckTest { + + @Test + @DisplayName("덱에서 카드를 뽑는다.") + void createDeckTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.ACE), + new Card(Shape.CLOVER, Rank.EIGHT), + new Card(Shape.DIAMOND, Rank.JACK) + ); + Deck deck = new Deck(cards); + Card expected = new Card(Shape.HEART, Rank.ACE); + // when, then + assertThat(deck.draw()).isEqualTo(expected); + } + + @Test + @DisplayName("덱에 카드가 없을 때 뽑는다면 예외를 발생시킨다.") + void emptyDeckDrawTest() { + // given + Deck deck = new Deck(List.of()); + // when, then + assertThatThrownBy(deck::draw) + .isInstanceOf(IllegalStateException.class) + .hasMessage("[ERROR] 덱이 비어있습니다."); + } + + @Test + @DisplayName("원하는 모양의 카드 전체가 생성된다.") + void createNumberCardsOfShapeTest() { + assertThat(Deck.createNumberCardsOf(Shape.CLOVER).size()).isEqualTo(13); + } + + @Test + @DisplayName("카드 전체가 생성된다.") + void createFullDeckTest() { + Deck deck = Deck.createShuffledFullDeck(); + + assertThat(deck.size()).isEqualTo(52); + } +} diff --git a/src/test/java/blackjack/game/CommandTest.java b/src/test/java/blackjack/game/CommandTest.java new file mode 100644 index 0000000000..6924de65f1 --- /dev/null +++ b/src/test/java/blackjack/game/CommandTest.java @@ -0,0 +1,32 @@ +package blackjack.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CommandTest { + + @Test + @DisplayName("명령어를 올바르게 변환한다.") + void convertCommandTest() { + // when + Command yes = Command.from("y"); + Command no = Command.from("n"); + // then + assertAll( + () -> assertThat(yes).isEqualTo(Command.YES), + () -> assertThat(no).isEqualTo(Command.NO) + ); + } + + @Test + @DisplayName("존재하지 않는 명령어가 주어지면 예외를 발생시킨다.") + void commandNotFoundTest() { + assertThatThrownBy(() -> Command.from("hi")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 존재하지 않는 명령어입니다."); + } +} diff --git a/src/test/java/blackjack/game/MatchResultTest.java b/src/test/java/blackjack/game/MatchResultTest.java new file mode 100644 index 0000000000..fffbd0d200 --- /dev/null +++ b/src/test/java/blackjack/game/MatchResultTest.java @@ -0,0 +1,104 @@ +package blackjack.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.card.Card; +import blackjack.card.Rank; +import blackjack.card.Shape; +import blackjack.player.Hand; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MatchResultTest { + + private static final Hand handBlackJack = new Hand(List.of( + new Card(Shape.CLOVER, Rank.ACE), + new Card(Shape.DIAMOND, Rank.TEN) + )); + private static final Hand hand22 = new Hand(List.of( + new Card(Shape.HEART, Rank.TEN), + new Card(Shape.CLOVER, Rank.TEN), + new Card(Shape.DIAMOND, Rank.TWO) + )); + private static final Hand hand21 = new Hand(List.of( + new Card(Shape.CLOVER, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN), + new Card(Shape.HEART, Rank.ACE) + )); + private static final Hand hand20 = new Hand(List.of( + new Card(Shape.CLOVER, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN) + )); + + @ParameterizedTest + @MethodSource("playerWinCases") + @DisplayName("플레이어가 배팅 금액만큼 돈을 버는 경우를 올바르게 판단한다.") + void playerWinTest(Hand playerHand, Hand dealerHand) { + double result = MatchResult.calculateRateOfPrize(playerHand, dealerHand); + + assertThat(result).isEqualTo(MatchResult.PLAYER_WIN.getRateOfPrize()); + } + + static Stream playerWinCases() { + return Stream.of( + Arguments.of(hand20, hand22), + Arguments.of(hand21, hand20) + ); + } + + @ParameterizedTest + @MethodSource("playerBlackJackCases") + @DisplayName("플레이어가 배팅 금액의 1.5배만큼 돈을 버는 경우를 올바르게 판단한다.") + void playerBlackJackTest(Hand playerHand, Hand dealerHand) { + double result = MatchResult.calculateRateOfPrize(playerHand, dealerHand); + + assertThat(result).isEqualTo(MatchResult.PLAYER_BLACKJACK.getRateOfPrize()); + } + + static Stream playerBlackJackCases() { + return Stream.of( + Arguments.of(handBlackJack, hand22), + Arguments.of(handBlackJack, hand21), + Arguments.of(handBlackJack, hand20) + ); + } + + @ParameterizedTest + @MethodSource("dealerWinCases") + @DisplayName("플레이어가 배팅 금액을 잃는 경우를 올바르게 판단한다.") + void dealerWinTest(Hand playerHand, Hand dealerHand) { + double result = MatchResult.calculateRateOfPrize(playerHand, dealerHand); + + assertThat(result).isEqualTo(MatchResult.DEALER_WIN.getRateOfPrize()); + } + + static Stream dealerWinCases() { + return Stream.of( + Arguments.of(hand22, hand20), + Arguments.of(hand22, hand22), + Arguments.of(hand20, hand21), + Arguments.of(hand21, handBlackJack) + ); + } + + @ParameterizedTest + @MethodSource("tieCases") + @DisplayName("플레이어가 배팅 금액을 돌려받는 경우를 올바르게 판단한다.") + void tieTest(Hand playerHand, Hand dealerHand) { + double result = MatchResult.calculateRateOfPrize(playerHand, dealerHand); + + assertThat(result).isEqualTo(MatchResult.TIE.getRateOfPrize()); + } + + static Stream tieCases() { + return Stream.of( + Arguments.of(handBlackJack, handBlackJack), + Arguments.of(hand21, hand21), + Arguments.of(hand20, hand20) + ); + } +} diff --git a/src/test/java/blackjack/game/MatchResultsTest.java b/src/test/java/blackjack/game/MatchResultsTest.java new file mode 100644 index 0000000000..4833c47181 --- /dev/null +++ b/src/test/java/blackjack/game/MatchResultsTest.java @@ -0,0 +1,54 @@ +package blackjack.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.card.Card; +import blackjack.card.Rank; +import blackjack.card.Shape; +import blackjack.player.Hand; +import blackjack.player.Player; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MatchResultsTest { + + private final Hand dealerHand = new Hand(List.of( + new Card(Shape.DIAMOND, Rank.TEN), + new Card(Shape.HEART, Rank.TEN + ))); + private final Hand playerHand = new Hand(List.of( + new Card(Shape.CLOVER, Rank.TEN), + new Card(Shape.DIAMOND, Rank.FIVE + ))); + private final Player player = new Player("atto", playerHand); + private final MatchResults matchResults = new MatchResults(dealerHand); + private final Money money = new Money(10000); + + @BeforeEach + void beforeEach() { + matchResults.addResult(player, money); + } + + @Test + @DisplayName("플레이어로 배팅 결과를 올바르게 가져온다.") + void getResultByNameTest() { + assertThat(matchResults.getResultOf(player)).isEqualTo(-10000); + } + + @Test + @DisplayName("플레이어가 존재하지 않는 경우 예외를 발생시킨다.") + void nameNotFoundTest() { + assertThatThrownBy(() -> matchResults.getResultOf(new Player("pobi"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 존재하지 않는 플레이어입니다."); + } + + @Test + @DisplayName("딜러의 수익을 구한다.") + void getDealerResultTest() { + assertThat(matchResults.getDealerResult()).isEqualTo(10000); + } +} diff --git a/src/test/java/blackjack/game/MoneyTest.java b/src/test/java/blackjack/game/MoneyTest.java new file mode 100644 index 0000000000..1215d7f936 --- /dev/null +++ b/src/test/java/blackjack/game/MoneyTest.java @@ -0,0 +1,19 @@ +package blackjack.game; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MoneyTest { + + @ParameterizedTest + @ValueSource(ints = {0, -1000}) + @DisplayName("금액이 양수가 아니면 예외를 발생시킨다.") + void validateNaturalTest(int money) { + assertThatThrownBy(() -> new Money(money)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 금액은 양수여야 합니다."); + } +} diff --git a/src/test/java/blackjack/game/PlayerBettingMoneyTest.java b/src/test/java/blackjack/game/PlayerBettingMoneyTest.java new file mode 100644 index 0000000000..75628faa1d --- /dev/null +++ b/src/test/java/blackjack/game/PlayerBettingMoneyTest.java @@ -0,0 +1,49 @@ +package blackjack.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.player.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayerBettingMoneyTest { + + private PlayerBettingMoney playerBettingMoney; + private Player player; + private Money money; + + @BeforeEach + void beforeEach() { + playerBettingMoney = new PlayerBettingMoney(); + player = new Player("atto"); + money = new Money(10000); + + playerBettingMoney.addBetting(player, money); + } + + @Test + @DisplayName("플레이어별 배팅 금액을 저장한다.") + void addBettingTest() { + assertThat(playerBettingMoney.getBettingAmountOf(player)).isEqualTo(money); + } + + @Test + @DisplayName("존재하지 않는 플레이어의 배팅 결과를 가져오려 할 경우 예외를 발생시킨다.") + void notExistPlayerTest() { + Player notExistPlayer = new Player("pobi"); + + assertThatThrownBy(() -> playerBettingMoney.getBettingAmountOf(notExistPlayer)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 존재하지 않는 플레이어입니다."); + } + + @Test + @DisplayName("이미 존재하는 플레이어의 배팅 결과를 저장하려 할 경우 예외를 발생시킨다.") + void alreadyExistPlayerTest() { + assertThatThrownBy(() -> playerBettingMoney.addBetting(player, money)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 이미 존재하는 플레이어입니다."); + } +} diff --git a/src/test/java/blackjack/player/DealerTest.java b/src/test/java/blackjack/player/DealerTest.java new file mode 100644 index 0000000000..d9897d57b5 --- /dev/null +++ b/src/test/java/blackjack/player/DealerTest.java @@ -0,0 +1,73 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.card.Card; +import blackjack.card.Rank; +import blackjack.card.Shape; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DealerTest { + + @Test + @DisplayName("딜러는 16점 이하이면 추가 드로우가 가능하다.") + void ableToDrawTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.JACK), + new Card(Shape.DIAMOND, Rank.SIX) + ); + Hand hand = new Hand(cards); + Dealer dealer = new Dealer(hand); + // when + boolean isDrawable = dealer.hasDrawableScore(); + // then + assertThat(isDrawable).isTrue(); + } + + @Test + @DisplayName("딜러는 16점 초과이면 추가 드로우가 불가능하다.") + void unableToDrawTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.JACK), + new Card(Shape.DIAMOND, Rank.SEVEN) + ); + Hand hand = new Hand(cards); + Dealer dealer = new Dealer(hand); + // when + boolean isDrawable = dealer.hasDrawableScore(); + // then + assertThat(isDrawable).isFalse(); + } + + @Test + @DisplayName("딜러의 첫 번째 카드를 가져온다.") + void getFirstCardTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.JACK), + new Card(Shape.DIAMOND, Rank.SEVEN) + ); + Hand hand = new Hand(cards); + Dealer dealer = new Dealer(hand); + // when, then + assertThat(dealer.getFirstCard()).isEqualTo(new Card(Shape.HEART, Rank.JACK)); + } + + @Test + @DisplayName("딜러가 카드를 가지고 있지 않을 때 가져가는 것을 시도하면 예외를 발생시킨다.") + void getFirstCardOnEmptyHandTest() { + // given + List cards = List.of(); + Hand hand = new Hand(cards); + Dealer dealer = new Dealer(hand); + // when, then + assertThatThrownBy(dealer::getFirstCard) + .isInstanceOf(IllegalStateException.class) + .hasMessage("[ERROR] 딜러가 카드를 가지고 있지 않습니다."); + } +} diff --git a/src/test/java/blackjack/player/HandTest.java b/src/test/java/blackjack/player/HandTest.java new file mode 100644 index 0000000000..9b4dbdda92 --- /dev/null +++ b/src/test/java/blackjack/player/HandTest.java @@ -0,0 +1,83 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.card.Card; +import blackjack.card.Rank; +import blackjack.card.Shape; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HandTest { + + @Test + @DisplayName("플레이어의 점수를 계산한다.") + void calculateScoreTest() { + // given + List cards = List.of( + new Card(Shape.SPADE, Rank.KING), + new Card(Shape.HEART, Rank.EIGHT) + ); + Hand hand = new Hand(cards); + // when, then + assertThat(hand.calculateScore()).isEqualTo(new Score(18)); + } + + @Test + @DisplayName("플레이어의 점수를 계산할 때, Ace의 점수를 유리한 방향(11)으로 결정한다.") + void calculateAceAsElevenTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.TEN), + new Card(Shape.CLOVER, Rank.ACE) + ); + Hand hand = new Hand(cards); + // when, then + assertThat(hand.calculateScore()).isEqualTo(new Score(21)); + } + + @Test + @DisplayName("플레이어의 점수를 계산할 때, Ace의 점수를 유리한 방향(1)으로 결정한다.") + void calculateAceAsOneTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.TEN), + new Card(Shape.CLOVER, Rank.ACE), + new Card(Shape.DIAMOND, Rank.TEN) + ); + Hand hand = new Hand(cards); + // when, then + assertThat(hand.calculateScore()).isEqualTo(new Score(21)); + } + + @Test + @DisplayName("블랙잭 여부를 확인한다.") + void isBlackJackTest() { + List cardsBlackJack = List.of( + new Card(Shape.CLOVER, Rank.ACE), + new Card(Shape.DIAMOND, Rank.TEN) + ); + Hand handBlackJack = new Hand(cardsBlackJack); + + List cards21 = List.of( + new Card(Shape.CLOVER, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN), + new Card(Shape.HEART, Rank.ACE) + ); + Hand hand21 = new Hand(cards21); + + List cards20 = List.of( + new Card(Shape.CLOVER, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN) + ); + Hand hand20 = new Hand(cards20); + + assertAll( + () -> assertThat(handBlackJack.isBlackJack()).isTrue(), + () -> assertThat(hand21.isBlackJack()).isFalse(), + () -> assertThat(hand20.isBlackJack()).isFalse() + ); + } +} diff --git a/src/test/java/blackjack/player/NameTest.java b/src/test/java/blackjack/player/NameTest.java new file mode 100644 index 0000000000..dc205b7b23 --- /dev/null +++ b/src/test/java/blackjack/player/NameTest.java @@ -0,0 +1,37 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + + @ParameterizedTest + @ValueSource(strings = {"12", "12345"}) + @DisplayName("이름이 잘 생성된다.") + void validNameTest(String name) { + assertDoesNotThrow(() -> new Name(name)); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("이름이 공백으로 이루어진 경우 예외를 발생시킨다.") + void blankNameTest(String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 이름은 공백일 수 없습니다."); + } + + @ParameterizedTest + @ValueSource(strings = {"a", "abcdef"}) + @DisplayName("이름이 길이 제한을 어기는 경우 예외를 발생시킨다.") + void longNameTest(String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 이름은 2글자 이상 5글자 이하여야 합니다."); + } +} diff --git a/src/test/java/blackjack/player/PlayerTest.java b/src/test/java/blackjack/player/PlayerTest.java new file mode 100644 index 0000000000..140bca5001 --- /dev/null +++ b/src/test/java/blackjack/player/PlayerTest.java @@ -0,0 +1,46 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.card.Card; +import blackjack.card.Rank; +import blackjack.card.Shape; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayerTest { + + @Test + @DisplayName("플레이어는 21점 미만이면 추가 드로우가 가능하다.") + void ableToDrawTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN) + ); + Hand hand = new Hand(cards); + Player player = new Player("aru", hand); + // when + boolean isDrawable = player.hasDrawableScore(); + // then + assertThat(isDrawable).isTrue(); + } + + @Test + @DisplayName("플렝이어는 21점 초과이면 추가 드로우가 불가능하다.") + void unableToDrawTest() { + // given + List cards = List.of( + new Card(Shape.HEART, Rank.JACK), + new Card(Shape.DIAMOND, Rank.TEN), + new Card(Shape.DIAMOND, Rank.TWO) + ); + Hand hand = new Hand(cards); + Player player = new Player("atto", hand); + // when + boolean isDrawable = player.hasDrawableScore(); + // then + assertThat(isDrawable).isFalse(); + } +} diff --git a/src/test/java/blackjack/player/PlayersTest.java b/src/test/java/blackjack/player/PlayersTest.java new file mode 100644 index 0000000000..92bf1268d2 --- /dev/null +++ b/src/test/java/blackjack/player/PlayersTest.java @@ -0,0 +1,51 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + @Test + @DisplayName("플레이어의 이름이 중복될 경우 예외를 발생시킨다") + void duplicateNameTest() { + // given + List names = List.of("aru", "atto", "aru"); + // when, then + assertThatThrownBy(() -> new Players(names)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 이름은 중복될 수 없습니다."); + } + + @Test + @DisplayName("플레이어의 수가 제한을 초과하는 경우 예외를 발생시킨다.") + void exceedingPlayersSizeTest() { + // given + List playerNames = List.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"); + // when, then + assertThatThrownBy(() -> new Players(playerNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 플레이어의 수는 1명 이상 10명 이하여야 합니다."); + } + + @Test + @DisplayName("플레이어가 없는 경우 예외를 발생시킨다.") + void emptyPlayersSizeTest() { + // given + List playerNames = List.of(); + // when, then + assertThatThrownBy(() -> new Players(playerNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 플레이어의 수는 1명 이상 10명 이하여야 합니다."); + } + + @Test + @DisplayName("플레이어로 null이 전달되는 경우 예외를 발생시킨다.") + void nullPlayerTest() { + assertThatThrownBy(() -> new Players(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 플레이어로 null이 전달되었습니다."); + } +} diff --git a/src/test/java/blackjack/player/ScoreTest.java b/src/test/java/blackjack/player/ScoreTest.java new file mode 100644 index 0000000000..c3141344aa --- /dev/null +++ b/src/test/java/blackjack/player/ScoreTest.java @@ -0,0 +1,76 @@ +package blackjack.player; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ScoreTest { + + @Test + @DisplayName("점수를 더할 수 있다.") + void addScoreTest() { + Score score = new Score(2); + + assertThat(score.add(3)).isEqualTo(new Score(5)); + } + + @Test + @DisplayName("점수가 버스트인지 판단한다.") + void isBustTest() { + Score bustScore = new Score(22); + Score notBustScore = new Score(21); + + assertAll( + () -> assertThat(bustScore.isBust()).isTrue(), + () -> assertThat(notBustScore.isBust()).isFalse() + ); + } + + @Test + @DisplayName("점수가 버스트인지 판단한다.") + void isNotBustTest() { + Score bustScore = new Score(22); + Score notBustScore = new Score(21); + + assertAll( + () -> assertThat(bustScore.isNotBust()).isFalse(), + () -> assertThat(notBustScore.isNotBust()).isTrue() + ); + } + + @Test + @DisplayName("점수의 대소비교가 가능하다.") + void isLargerThanTest() { + Score score = new Score(15); + + assertAll( + () -> assertThat(score.isLargerThan(new Score(14))).isTrue(), + () -> assertThat(score.isLargerThan(new Score(16))).isFalse() + ); + } + + @Test + @DisplayName("점수의 대소비교가 가능하다.") + void isSmallerThanEqualTest() { + Score score = new Score(15); + + assertAll( + () -> assertThat(score.isSmallerThanOrEqualTo(new Score(14))).isFalse(), + () -> assertThat(score.isSmallerThanOrEqualTo(new Score(16))).isTrue() + ); + } + + @Test + @DisplayName("보정된 에이스 점수를 계산한다.") + void changeToLargeAceTest() { + Score canChangeScore = new Score(11); + Score cantChangeScore = new Score(12); + + assertAll( + () -> assertThat(canChangeScore.changeToLargeAceScore()).isEqualTo(new Score(21)), + () -> assertThat(cantChangeScore.changeToLargeAceScore()).isEqualTo(new Score(12)) + ); + } +}