diff --git a/README.md b/README.md index 556099c4de3..f0776d7826a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,50 @@ 블랙잭 미션 저장소 -## 우아한테크코스 코드리뷰 +## 기능 요구 사항 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +### 게임 + +- [x] 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 쪽이 승리한다. +- [x] 게임을 시작하면 플레이어는 두 장의 카드를 지급 받는다. + - [x] 이후 플레이어와 딜러의 카드를 출력한다. + - [x] 단, 딜러의 카드는 하나만 출력한다. +- [x] 플레이어는 게임을 시작할 때 원하는 금액을 배팅한다. +- [x] 플레이어는 카드의 숫자 합이 21을 초과하지 않는다면 카드를 원하는 만큼 다시 뽑을 수 있다. + - [x] 새로 받을 때 마다 해당 플레이어의 카드를 출력한다. +- [x] 플레이어가 카드를 다 받으면 딜러의 카드를 확인한다. +- [x] 딜러의 카드 합이 17 이상이 될 때 까지 카드를 받는다. +- [x] 플레이어의 게임 결과로부터 수익을 계산한다. + - [x] 블랙잭으로 승리 할 경우 배팅 금액의 1.5배를 딜러로부터 받는다. + - [x] 무승부인 경우 플레이어는 베팅한 금액을 돌려받는다. + - [x] 승리할 경우 베팅 금액만큼 받는다. + - [x] 패배할 경우 배팅 금액을 모두 잃는다. +- [x] 딜러와 플레이어의 카드, 결과와 최종 수익 결과를 출력한다. + +### 카드 + +- [x] 클로버, 스페이드, 하트, 다이아몬드 모양을 가진다. +- [x] 카드는 2부터 10까지의 숫자와 Ace, King, Queen, Jack으로 이루어져 있다. +- [x] King, Queen, Jack은 10으로 계산한다. +- [x] ACE는 1 또는 11로 계산할 수 있다. + +### 딜러, 플레이어 공통 + +- [x] 최소 1글자, 최대 5글자의 이름을 가진다. +- [x] ACE 카드를 가지는 경우, 일단 11로 계산한 뒤, 합이 21을 초과하면 1로 계산한다. +- [x] 현재 가진 카드 숫자의 합을 기준으로 카드를 더 받을 수 있는지 결정한다. + - [x] 딜러는 숫자의 합이 16 이하이면 카드를 더 받을 수 있다. + - [x] 플레이어는 숫자의 합이 21 이하이면 카드를 더 받을 수 있다. + +### 플레이어 + +- [x] 플레이어의 이름은 중복될 수 없다. +- [x] 플레이어는 최소 2명부터 최대 8명까지 가능하다. +- [x] 딜러의 점수를 입력받아 승,무,패를 결정한다. + - [x] (블랙잭) 처음 두 장의 카드 합이 21일 경우 블랙잭. + - [x] (무승부) 딜러와 플레이어가 동시에 블랙잭인 경우. + - [x] (무승부) 플레이어의 점수가 21 이하이고, 딜러와 동점인 경우. + - [x] (승리) 플레이어의 점수가 21 이하이고, 딜러의 점수가 21을 초과하는 경우. + - [x] (승리) 플레이어와 딜러의 점수가 모두 21 이하이고, 딜러의 점수보다 큰 경우. + - [x] (패배) 플레이어의 점수가 21을 초과하면 딜러의 점수와 무관. + - [x] (패배) 플레이어와 딜러의 점수가 모두 21 이하이고, 딜러의 점수보다 작은 경우. diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..2ddbf1df987 --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,11 @@ +package blackjack; + +import blackjack.controller.BlackjackController; + +public class Application { + + public static void main(String[] args) { + BlackjackController blackjackController = new BlackjackController(); + blackjackController.run(); + } +} diff --git a/src/main/java/blackjack/controller/BlackjackController.java b/src/main/java/blackjack/controller/BlackjackController.java new file mode 100644 index 00000000000..19a8f915697 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,98 @@ +package blackjack.controller; + +import blackjack.domain.game.BlackjackGame; +import blackjack.domain.game.Money; +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.Player; +import blackjack.domain.gamer.Players; +import blackjack.dto.DealerInitialHandDto; +import blackjack.dto.HandDto; +import blackjack.dto.PlayerGameResultsDto; +import blackjack.dto.PlayerHandDto; +import blackjack.dto.PlayersHandDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import blackjack.view.object.Command; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BlackjackController { + + private final InputView inputView; + private final OutputView outputView; + private final BlackjackGame blackjackGame; + + public BlackjackController() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.blackjackGame = new BlackjackGame(); + } + + public void run() { + Players players = getPlayers(); + Dealer dealer = new Dealer(); + blackjackGame.distributeInitialHand(players, dealer); + blackjackGame.betPlayerMoney(receivePlayersBetMoney(players)); + printInitialHands(players, dealer); + + distributeCardToPlayers(players); + blackjackGame.distributeCardToDealer(dealer); + + printDealerCanReceiveCardMessage(dealer); + printAllGamerScores(dealer, players); + printResult(players, dealer); + } + + private Players getPlayers() { + List playerNames = inputView.receivePlayerNames(); + return new Players(playerNames); + } + + private Map receivePlayersBetMoney(Players players) { + Map playerBetMoney = new HashMap<>(); + for (Player player : players.getPlayers()) { + int betMoney = inputView.receivePlayerMoney(player.getName().value()); + playerBetMoney.put(player, new Money(betMoney)); + } + return playerBetMoney; + } + + private void printInitialHands(Players players, Dealer dealer) { + outputView.printInitialHands(DealerInitialHandDto.fromDealer(dealer), PlayersHandDto.fromPlayers(players)); + } + + private void distributeCardToPlayers(Players players) { + for (Player player : players.getPlayers()) { + distributeCardToPlayer(player); + } + } + + private void distributeCardToPlayer(Player player) { + while (player.canReceiveCard() && Command.isHit(getCommand(player))) { + blackjackGame.addCardToPlayer(player); + outputView.printPlayerHand(PlayerHandDto.fromPlayer(player)); + } + } + + private Command getCommand(Player player) { + return inputView.receiveCommand(player.getName().value()); + } + + private void printDealerCanReceiveCardMessage(Dealer dealer) { + if (dealer.canReceiveCard()) { + outputView.printDealerMessage(); + } + } + + private void printAllGamerScores(Dealer dealer, Players players) { + outputView.printDealerHandScore(HandDto.fromHand(dealer.getHand())); + outputView.printPlayersHandScore(PlayersHandDto.fromPlayers(players)); + } + + private void printResult(Players players, Dealer dealer) { + Money dealerIncome = blackjackGame.calculateDealerIncome(players, dealer); + PlayerGameResultsDto playerGameResultsDto = PlayerGameResultsDto.fromPlayerBetResults(blackjackGame.getStore()); + outputView.printResult(dealerIncome.value(), playerGameResultsDto); + } +} 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..8a649540af7 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,47 @@ +package blackjack.domain.card; + +import java.util.Objects; + +public class Card { + + private final CardShape cardShape; + private final CardNumber cardNumber; + + public Card(CardShape cardShape, CardNumber cardNumber) { + this.cardShape = cardShape; + this.cardNumber = cardNumber; + } + + public boolean isAce() { + return cardNumber == CardNumber.ACE; + } + + public int getNumberValue() { + return cardNumber.getValue(); + } + + public CardShape getCardShape() { + return cardShape; + } + + public CardNumber getCardNumber() { + return cardNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Card card = (Card) o; + return cardShape == card.cardShape && cardNumber == card.cardNumber; + } + + @Override + public int hashCode() { + return Objects.hash(cardShape, cardNumber); + } +} diff --git a/src/main/java/blackjack/domain/card/CardNumber.java b/src/main/java/blackjack/domain/card/CardNumber.java new file mode 100644 index 00000000000..044a8bb42f7 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardNumber.java @@ -0,0 +1,28 @@ +package blackjack.domain.card; + +public enum CardNumber { + + ACE(11), + 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 int value; + + CardNumber(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/blackjack/domain/card/CardShape.java b/src/main/java/blackjack/domain/card/CardShape.java new file mode 100644 index 00000000000..35169f3e95b --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardShape.java @@ -0,0 +1,9 @@ +package blackjack.domain.card; + +public enum CardShape { + + HEART, + CLOVER, + SPADE, + DIAMOND +} 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..7c582159af7 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,36 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class Deck { + + private static final List CACHE = Arrays.stream(CardShape.values()) + .flatMap(cardShape -> Arrays.stream(CardNumber.values()) + .map(number -> new Card(cardShape, number))).toList(); + + private final LinkedList cards; + + public Deck() { + this.cards = new LinkedList<>(CACHE); + } + + Deck(LinkedList cards) { + this.cards = cards; + } + + public void shuffle() { + Collections.shuffle(cards); + } + + public Card draw() { + return cards.poll(); + } + + public List getCards() { + return new ArrayList<>(cards); + } +} diff --git a/src/main/java/blackjack/domain/game/BlackjackGame.java b/src/main/java/blackjack/domain/game/BlackjackGame.java new file mode 100644 index 00000000000..6a24b501aff --- /dev/null +++ b/src/main/java/blackjack/domain/game/BlackjackGame.java @@ -0,0 +1,60 @@ +package blackjack.domain.game; + +import blackjack.domain.card.Deck; +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Player; +import blackjack.domain.gamer.Players; +import java.util.Map; + +public class BlackjackGame { + + private final GameAccount gameAccount; + private final Deck deck; + + public BlackjackGame() { + this.gameAccount = new GameAccount(); + this.deck = new Deck(); + } + + public void distributeInitialHand(Players players, Dealer dealer) { + deck.shuffle(); + setUpInitialHands(players, dealer); + } + + + private void setUpInitialHands(Players players, Dealer dealer) { + players.initAllPlayersCard(deck); + dealer.initCard(deck); + } + + public void betPlayerMoney(Map playersBetMoney) { + for (Player player : playersBetMoney.keySet()) { + gameAccount.betMoney(player, playersBetMoney.get(player)); + } + } + + public void addCardToPlayer(Player player) { + player.addCard(deck.draw()); + } + + public void distributeCardToDealer(Dealer dealer) { + while (dealer.canReceiveCard()) { + dealer.addCard(deck.draw()); + } + } + + public Money calculateDealerIncome(Players players, Dealer dealer) { + applyResultToBetMoney(players, dealer); + return gameAccount.calculateDealerIncome(); + } + + private void applyResultToBetMoney(Players players, Dealer dealer) { + Map playerGameResults = players.collectPlayerGameResults(dealer.getHandValue()); + gameAccount.applyGameResults(playerGameResults); + } + + public Map getStore() { + return gameAccount.getStore(); + } +} diff --git a/src/main/java/blackjack/domain/game/GameAccount.java b/src/main/java/blackjack/domain/game/GameAccount.java new file mode 100644 index 00000000000..95373698ec2 --- /dev/null +++ b/src/main/java/blackjack/domain/game/GameAccount.java @@ -0,0 +1,44 @@ +package blackjack.domain.game; + +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Player; +import java.util.LinkedHashMap; +import java.util.Map; + +public class GameAccount { + + private static final Map store = new LinkedHashMap<>(); + + public void betMoney(Player player, Money money) { + store.put(player, money); + } + + public Money findMoney(Player player) { + return store.get(player); + } + + public void applyGameResults(Map gameResults) { + for (Player player : gameResults.keySet()) { + Money money = store.get(player); + GameResult gameResult = gameResults.get(player); + Money gameResultMoney = money.multipleRatio(gameResult.getRatio()); + store.put(player, gameResultMoney); + } + } + + public Money calculateDealerIncome() { + int dealerIncome = 0; + for (Money money : store.values()) { + dealerIncome += money.value(); + } + return new Money(-dealerIncome); + } + + public Map getStore() { + return new LinkedHashMap<>(store); + } + + public void clearStore() { + store.clear(); + } +} diff --git a/src/main/java/blackjack/domain/game/Money.java b/src/main/java/blackjack/domain/game/Money.java new file mode 100644 index 00000000000..ad2c5512a67 --- /dev/null +++ b/src/main/java/blackjack/domain/game/Money.java @@ -0,0 +1,8 @@ +package blackjack.domain.game; + +public record Money(int value) { + + public Money multipleRatio(double ratio) { + return new Money((int) (this.value * ratio)); + } +} diff --git a/src/main/java/blackjack/domain/gamer/BlackjackGamer.java b/src/main/java/blackjack/domain/gamer/BlackjackGamer.java new file mode 100644 index 00000000000..2de6d351233 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/BlackjackGamer.java @@ -0,0 +1,41 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import java.util.ArrayList; + +public abstract class BlackjackGamer { + + private final Hand hand; + + public BlackjackGamer() { + this.hand = new Hand(new ArrayList<>()); + } + + public BlackjackGamer(Hand hand) { + this.hand = hand; + } + + public abstract boolean canReceiveCard(); + + public void initCard(Deck deck) { + addCard(deck.draw()); + addCard(deck.draw()); + } + + public void addCard(Card card) { + hand.add(card); + } + + public Hand getHand() { + return hand; + } + + public HandValue getHandValue() { + return hand.generateValue(); + } + + public int getScore() { + return getHandValue().getScore(); + } +} diff --git a/src/main/java/blackjack/domain/gamer/Dealer.java b/src/main/java/blackjack/domain/gamer/Dealer.java new file mode 100644 index 00000000000..9b62291a3d7 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Dealer.java @@ -0,0 +1,21 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Card; + +public class Dealer extends BlackjackGamer { + + private static final int DEALER_DRAW_THRESHOLD = 16; + + public Dealer() { + super(); + } + + @Override + public boolean canReceiveCard() { + return getScore() <= DEALER_DRAW_THRESHOLD; + } + + public Card getFirstCard() { + return getHand().getFirstCard(); + } +} diff --git a/src/main/java/blackjack/domain/gamer/GameResult.java b/src/main/java/blackjack/domain/gamer/GameResult.java new file mode 100644 index 00000000000..6bd62072d42 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/GameResult.java @@ -0,0 +1,19 @@ +package blackjack.domain.gamer; + +public enum GameResult { + + WIN(1.0), + LOSE(-1.0), + DRAW(0), + BLACKJACK_WIN(1.5); + + private final double ratio; + + GameResult(double ratio) { + this.ratio = ratio; + } + + public double getRatio() { + return ratio; + } +} diff --git a/src/main/java/blackjack/domain/gamer/Hand.java b/src/main/java/blackjack/domain/gamer/Hand.java new file mode 100644 index 00000000000..4ee7fdcab34 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Hand.java @@ -0,0 +1,30 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Card; +import java.util.ArrayList; +import java.util.List; + +public class Hand { + + private final List cards; + + public Hand(List cards) { + this.cards = cards; + } + + public void add(Card card) { + cards.add(card); + } + + public HandValue generateValue() { + return new HandValue(cards); + } + + public Card getFirstCard() { + return cards.get(0); + } + + public List getCards() { + return new ArrayList<>(cards); + } +} diff --git a/src/main/java/blackjack/domain/gamer/HandValue.java b/src/main/java/blackjack/domain/gamer/HandValue.java new file mode 100644 index 00000000000..0d31968ce81 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/HandValue.java @@ -0,0 +1,92 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Card; +import java.util.List; + +public class HandValue { + + private static final int BLACKJACK_MAX_SCORE = 21; + private static final int BLACKJACK_HAND_SIZE_CONDITION = 2; + private static final int ACE_VALUE_MODIFIER = 10; + + private final int score; + private final int handSize; + + public HandValue(List cards) { + this.score = calculateScore(cards); + this.handSize = cards.size(); + } + + HandValue(int score, int handSize) { + this.score = score; + this.handSize = handSize; + } + + public int calculateScore(List cards) { + int sum = cards.stream() + .mapToInt(Card::getNumberValue) + .sum(); + + if (hasAce(cards)) { + return adjustSumWithAce(cards, sum); + } + return sum; + } + + private boolean hasAce(List cards) { + return cards.stream() + .anyMatch(Card::isAce); + } + + /** + * ACE가 포함된 경우, 21 이하이면서 가장 가능한 큰 값으로 계산한다. + */ + private int adjustSumWithAce(List cards, int sum) { + int aceCount = (int) cards.stream() + .filter(Card::isAce) + .count(); + + for (int i = 0; i < aceCount; i++) { + sum = adjust(sum); + } + return sum; + } + + /** + * Ace는 기본적으로 11로 계산되나, 합계가 21을 초과할 경우 1로 계산한다. + */ + private int adjust(int sum) { + if (sum > BLACKJACK_MAX_SCORE) { + sum -= ACE_VALUE_MODIFIER; + } + return sum; + } + + public boolean isBlackjack() { + return score == BLACKJACK_MAX_SCORE && handSize == BLACKJACK_HAND_SIZE_CONDITION; + } + + public boolean isNotBlackjack() { + return !isBlackjack(); + } + + public boolean isBust() { + return score > BLACKJACK_MAX_SCORE; + } + + public boolean isNotBust() { + return !isBust(); + } + + public boolean isHigherThan(HandValue other) { + return this.score > other.score; + } + + public boolean isLowerThan(HandValue other) { + return this.score < other.score; + } + + public int getScore() { + return score; + } +} diff --git a/src/main/java/blackjack/domain/gamer/Name.java b/src/main/java/blackjack/domain/gamer/Name.java new file mode 100644 index 00000000000..2974903b49c --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Name.java @@ -0,0 +1,12 @@ +package blackjack.domain.gamer; + +public record Name(String value) { + + private static final int MAX_NAME_LENGTH = 5; + + public Name { + if (value.isBlank() || value.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름의 길이는 최소 1글자부터 최대 " + MAX_NAME_LENGTH + "글자까지 가능합니다."); + } + } +} diff --git a/src/main/java/blackjack/domain/gamer/Player.java b/src/main/java/blackjack/domain/gamer/Player.java new file mode 100644 index 00000000000..47b8a6c86f6 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Player.java @@ -0,0 +1,52 @@ +package blackjack.domain.gamer; + +public class Player extends BlackjackGamer { + + private static final int BLACKJACK_MAX_SCORE = 21; + + private final Name name; + + public Player(String name) { + this.name = new Name(name); + } + + public Player(Hand hand, Name name) { + super(hand); + this.name = name; + } + + @Override + public boolean canReceiveCard() { + return getScore() < BLACKJACK_MAX_SCORE; + } + + public GameResult compete(HandValue dealerHandValue) { + HandValue playerHandValue = getHandValue(); + if (isOnlyPlayerBlackjack(playerHandValue, dealerHandValue)) { + return GameResult.BLACKJACK_WIN; + } + if (playerHandValue.isBust() || isLowerThanDealerHandValue(playerHandValue, dealerHandValue)) { + return GameResult.LOSE; + } + if (dealerHandValue.isBust() || isHigherThanDealerHandValue(playerHandValue, dealerHandValue)) { + return GameResult.WIN; + } + return GameResult.DRAW; + } + + private boolean isOnlyPlayerBlackjack(HandValue playerHandValue, HandValue dealerHandValue) { + return playerHandValue.isBlackjack() && dealerHandValue.isNotBlackjack(); + } + + private boolean isLowerThanDealerHandValue(HandValue playerHandValue, HandValue dealerHandValue) { + return dealerHandValue.isNotBust() && playerHandValue.isLowerThan(dealerHandValue); + } + + private boolean isHigherThanDealerHandValue(HandValue playerHandValue, HandValue dealerHandValue) { + return playerHandValue.isNotBust() && playerHandValue.isHigherThan(dealerHandValue); + } + + public Name getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/gamer/Players.java b/src/main/java/blackjack/domain/gamer/Players.java new file mode 100644 index 00000000000..e4731f7abc0 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Players.java @@ -0,0 +1,60 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Deck; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Players { + + private static final int MIN_PLAYER_COUNT = 2; + private static final int MAX_PLAYER_COUNT = 8; + + private final List players; + + public Players(List names) { + validateNames(names); + + this.players = names.stream() + .map(Player::new) + .toList(); + } + + private void validateNames(List names) { + validateDuplicate(names); + validateCount(names.size()); + } + + private void validateCount(int count) { + if (count < MIN_PLAYER_COUNT || count > MAX_PLAYER_COUNT) { + throw new IllegalArgumentException( + "플레이어는 최소 " + MIN_PLAYER_COUNT + "명에서 최대 " + MAX_PLAYER_COUNT + "명까지 가능합니다" + ); + } + } + + private void validateDuplicate(List names) { + Set nonDuplicateNames = new HashSet<>(names); + + if (names.size() != nonDuplicateNames.size()) { + throw new IllegalArgumentException("이름은 중복될 수 없습니다."); + } + } + + public void initAllPlayersCard(Deck deck) { + players.forEach(player -> player.initCard(deck)); + } + + public Map collectPlayerGameResults(HandValue dealerHandValue) { + Map playerGameResults = new LinkedHashMap<>(); + players.forEach(player -> playerGameResults.put(player, player.compete(dealerHandValue))); + + return playerGameResults; + } + + public List getPlayers() { + return List.copyOf(players); + } +} diff --git a/src/main/java/blackjack/dto/CardDto.java b/src/main/java/blackjack/dto/CardDto.java new file mode 100644 index 00000000000..274c3a9b632 --- /dev/null +++ b/src/main/java/blackjack/dto/CardDto.java @@ -0,0 +1,12 @@ +package blackjack.dto; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; + +public record CardDto(CardNumber cardNumber, CardShape cardShape) { + + public static CardDto fromCard(Card card) { + return new CardDto(card.getCardNumber(), card.getCardShape()); + } +} diff --git a/src/main/java/blackjack/dto/DealerInitialHandDto.java b/src/main/java/blackjack/dto/DealerInitialHandDto.java new file mode 100644 index 00000000000..da041600b21 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerInitialHandDto.java @@ -0,0 +1,13 @@ +package blackjack.dto; + +import blackjack.domain.card.Card; +import blackjack.domain.gamer.Dealer; + +public record DealerInitialHandDto(CardDto firstCard) { + + public static DealerInitialHandDto fromDealer(Dealer dealer) { + Card first = dealer.getFirstCard(); + + return new DealerInitialHandDto(CardDto.fromCard(first)); + } +} diff --git a/src/main/java/blackjack/dto/HandDto.java b/src/main/java/blackjack/dto/HandDto.java new file mode 100644 index 00000000000..0ffc9b4be13 --- /dev/null +++ b/src/main/java/blackjack/dto/HandDto.java @@ -0,0 +1,15 @@ +package blackjack.dto; + +import blackjack.domain.gamer.Hand; +import java.util.List; + +public record HandDto(List cardDtos, int handScore) { + + public static HandDto fromHand(Hand hand) { + List cardDtos = hand.getCards().stream() + .map(CardDto::fromCard) + .toList(); + + return new HandDto(cardDtos, hand.generateValue().getScore()); + } +} diff --git a/src/main/java/blackjack/dto/PlayerGameResultsDto.java b/src/main/java/blackjack/dto/PlayerGameResultsDto.java new file mode 100644 index 00000000000..91b54067564 --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerGameResultsDto.java @@ -0,0 +1,19 @@ +package blackjack.dto; + +import blackjack.domain.game.Money; +import blackjack.domain.gamer.Player; +import java.util.LinkedHashMap; +import java.util.Map; + +public record PlayerGameResultsDto(Map playerIncomeResults) { + + public static PlayerGameResultsDto fromPlayerBetResults(Map playerBetResults) { + Map playerIncomeResults = new LinkedHashMap<>(); + + for (Player player : playerBetResults.keySet()) { + playerIncomeResults.put(player.getName().value(), playerBetResults.get(player).value()); + } + + return new PlayerGameResultsDto(playerIncomeResults); + } +} diff --git a/src/main/java/blackjack/dto/PlayerHandDto.java b/src/main/java/blackjack/dto/PlayerHandDto.java new file mode 100644 index 00000000000..bcf77cd0491 --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerHandDto.java @@ -0,0 +1,13 @@ +package blackjack.dto; + +import blackjack.domain.gamer.Player; + +public record PlayerHandDto(String name, HandDto playerHand) { + + public static PlayerHandDto fromPlayer(Player player) { + String playerName = player.getName().value(); + HandDto playerHand = HandDto.fromHand(player.getHand()); + + return new PlayerHandDto(playerName, playerHand); + } +} diff --git a/src/main/java/blackjack/dto/PlayersHandDto.java b/src/main/java/blackjack/dto/PlayersHandDto.java new file mode 100644 index 00000000000..cdbd5c50714 --- /dev/null +++ b/src/main/java/blackjack/dto/PlayersHandDto.java @@ -0,0 +1,15 @@ +package blackjack.dto; + +import blackjack.domain.gamer.Players; +import java.util.List; + +public record PlayersHandDto(List playersHandDto) { + + public static PlayersHandDto fromPlayers(Players players) { + List playerHandDtos = players.getPlayers().stream() + .map(PlayerHandDto::fromPlayer) + .toList(); + + return new PlayersHandDto(playerHandDtos); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..e00c7ff40b3 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,30 @@ +package blackjack.view; + +import blackjack.view.object.Command; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private final Scanner scanner; + + public InputView() { + scanner = new Scanner(System.in); + } + + public List receivePlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String input = scanner.nextLine(); + return List.of(input.split(",")); + } + + public int receivePlayerMoney(String name) { + System.out.printf("\n%s의 베팅 금액은?\n", name); + return Integer.parseInt(scanner.nextLine()); + } + + public Command receiveCommand(String name) { + System.out.printf("%s는(은) 한장의 카드를 더 받겠습니까?(예는 y, 아니오: n)\n", name); + return Command.convertInputToCommand(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 00000000000..4b261616b15 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,114 @@ +package blackjack.view; + +import blackjack.dto.CardDto; +import blackjack.dto.DealerInitialHandDto; +import blackjack.dto.HandDto; +import blackjack.dto.PlayerGameResultsDto; +import blackjack.dto.PlayerHandDto; +import blackjack.dto.PlayersHandDto; +import blackjack.view.object.CardNumberOutput; +import blackjack.view.object.CardShapeOutput; +import java.util.List; + +public class OutputView { + + private static final String DEALER_DEFAULT_NAME = "딜러"; + + public void printInitialHands(DealerInitialHandDto dealerInitialHandDto, PlayersHandDto playersInitialHandDto) { + System.out.printf("\n%s와 %s에게 2장을 나누었습니다.\n", DEALER_DEFAULT_NAME, + joinPlayerNames(playersInitialHandDto)); + + System.out.printf("%s 카드: %s\n", DEALER_DEFAULT_NAME, formatCardName(dealerInitialHandDto.firstCard())); + printAllPlayerHands(playersInitialHandDto); + } + + private String joinPlayerNames(PlayersHandDto playersInitialHandDto) { + return String.join(", ", getPlayerNames(playersInitialHandDto)); + } + + private List getPlayerNames(PlayersHandDto playersInitialHandDto) { + return playersInitialHandDto.playersHandDto().stream().map(PlayerHandDto::name).toList(); + } + + private String formatCardName(CardDto cardDto) { + return CardNumberOutput.convertNumberToOutput(cardDto.cardNumber()) + CardShapeOutput.convertShapeToOutput( + cardDto.cardShape()); + } + + private void printAllPlayerHands(PlayersHandDto playersInitialHandDto) { + playersInitialHandDto.playersHandDto().forEach(this::printPlayerHand); + System.out.println(); + } + + public void printPlayerHand(PlayerHandDto playerHandDto) { + System.out.println(getPlayerNameOutputFormat(playerHandDto)); + } + + public void printDealerHand(HandDto dealerHandDto) { + System.out.println(getDealerNameOutputFormat(dealerHandDto)); + } + + private StringBuilder getPlayerNameOutputFormat(PlayerHandDto playerHandDto) { + StringBuilder stringBuilder = new StringBuilder(); + return stringBuilder + .append(playerHandDto.name()) + .append("카드: ") + .append(joinCardNames(playerHandDto.playerHand())); + } + + private StringBuilder getDealerNameOutputFormat(HandDto dealerHandDto) { + StringBuilder stringBuilder = new StringBuilder(); + return stringBuilder + .append(DEALER_DEFAULT_NAME) + .append("카드: ") + .append(joinCardNames(dealerHandDto)); + } + + private String joinCardNames(HandDto handDto) { + return String.join(", ", getCardNames(handDto)); + } + + private List getCardNames(HandDto handDto) { + return handDto.cardDtos().stream().map(this::formatCardName).toList(); + } + + public void printPlayersHandScore(PlayersHandDto playersHandDto) { + for (PlayerHandDto playerHandDto : playersHandDto.playersHandDto()) { + System.out.println(getPlayerNameOutputFormat(playerHandDto) + .append(" - 결과: ") + .append(playerHandDto.playerHand().handScore())); + } + } + + public void printDealerHandScore(HandDto dealerHandDto) { + System.out.println(); + System.out.println(getDealerNameOutputFormat(dealerHandDto) + .append(" - 결과: ") + .append(dealerHandDto.handScore())); + } + + public void printDealerMessage() { + System.out.printf("\n%s는 16이하라 한장의 카드를 더 받았습니다.\n", DEALER_DEFAULT_NAME); + } + + public void printResult(int dealerIncome, PlayerGameResultsDto playerGameResultsDto) { + System.out.println("\n## 최종 수익"); + printDealerResult(dealerIncome); + printPlayerResults(playerGameResultsDto); + } + + private void printDealerResult(int dealerIncome) { + String stringBuilder = DEALER_DEFAULT_NAME + ": " + dealerIncome; + System.out.println(stringBuilder); + } + + private void printPlayerResults(PlayerGameResultsDto playerGameResultsDto) { + playerGameResultsDto.playerIncomeResults().forEach((name, money) -> + System.out.println(name + ": " + money) + ); + } + + public void printEmptyLine() { + System.out.println(); + } +} diff --git a/src/main/java/blackjack/view/object/CardNumberOutput.java b/src/main/java/blackjack/view/object/CardNumberOutput.java new file mode 100644 index 00000000000..03d4fa840f6 --- /dev/null +++ b/src/main/java/blackjack/view/object/CardNumberOutput.java @@ -0,0 +1,37 @@ +package blackjack.view.object; + +import blackjack.domain.card.CardNumber; +import java.util.Arrays; + +public enum CardNumberOutput { + + ACE(CardNumber.ACE ,"A"), + TWO(CardNumber.TWO, "2"), + THREE(CardNumber.THREE, "3"), + FOUR(CardNumber.FOUR, "4"), + FIVE(CardNumber.FIVE, "5"), + SIX(CardNumber.SIX, "6"), + SEVEN(CardNumber.SEVEN, "7"), + EIGHT(CardNumber.EIGHT, "8"), + NINE(CardNumber.NINE, "9"), + TEN(CardNumber.TEN, "10"), + JACK(CardNumber.JACK, "J"), + QUEEN(CardNumber.QUEEN, "Q"), + KING(CardNumber.KING, "K"); + + private final CardNumber cardNumber; + private final String output; + + CardNumberOutput(CardNumber cardNumber, String output) { + this.cardNumber = cardNumber; + this.output = output; + } + + public static String convertNumberToOutput(CardNumber cardNumber) { + return Arrays.stream(values()) + .filter(cardNumberOutput -> cardNumberOutput.cardNumber == cardNumber) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카드 숫자입니다.")) + .output; + } +} diff --git a/src/main/java/blackjack/view/object/CardShapeOutput.java b/src/main/java/blackjack/view/object/CardShapeOutput.java new file mode 100644 index 00000000000..38b0374ce34 --- /dev/null +++ b/src/main/java/blackjack/view/object/CardShapeOutput.java @@ -0,0 +1,28 @@ +package blackjack.view.object; + +import blackjack.domain.card.CardShape; +import java.util.Arrays; + +public enum CardShapeOutput { + + HEART_OUTPUT(CardShape.HEART, "하트"), + CLOVER_OUTPUT(CardShape.CLOVER, "클로버"), + SPADE_OUTPUT(CardShape.SPADE, "스페이드"), + DIAMOND_OUTPUT(CardShape.DIAMOND, "다이아몬드"); + + private final CardShape cardShape; + private final String output; + + CardShapeOutput(CardShape cardShape, String output) { + this.cardShape = cardShape; + this.output = output; + } + + public static String convertShapeToOutput(CardShape cardShape) { + return Arrays.stream(values()) + .filter(cardShapeOutput -> cardShapeOutput.cardShape == cardShape) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카드 문양입니다.")) + .output; + } +} diff --git a/src/main/java/blackjack/view/object/Command.java b/src/main/java/blackjack/view/object/Command.java new file mode 100644 index 00000000000..670cdbccf70 --- /dev/null +++ b/src/main/java/blackjack/view/object/Command.java @@ -0,0 +1,26 @@ +package blackjack.view.object; + +import java.util.Arrays; + +public enum Command { + + HIT("y"), + STAND("n"); + + private final String name; + + Command(String name) { + this.name = name; + } + + public static Command convertInputToCommand(String input) { + return Arrays.stream(values()) + .filter(command -> command.name.equals(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("입력과 일치하는 명령어가 존재하지 않습니다.")); + } + + public static boolean isHit(Command command) { + return command == HIT; + } +} 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..3cb79391241 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Objects; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CardTest { + + @Test + @DisplayName("equals & hashCode 재정의 테스트") + void equalsTest() { + Card card = new Card(CardShape.HEART, CardNumber.KING); + + assertThat(card).isEqualTo(new Card(CardShape.HEART, CardNumber.KING)); + assertThat(Objects.hash(card)).isEqualTo(Objects.hash(new Card(CardShape.HEART, CardNumber.KING))); + } +} 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..358bd4d9f03 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,41 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DeckTest { + + private Deck mockDeck; + private Deck realDeck; + + @BeforeEach + void setUP() { + LinkedList cardList = new LinkedList<>(List.of( + new Card(CardShape.HEART, CardNumber.KING), new Card(CardShape.HEART, CardNumber.QUEEN), + new Card(CardShape.HEART, CardNumber.ACE), new Card(CardShape.HEART, CardNumber.TWO), + new Card(CardShape.DIAMOND, CardNumber.KING), new Card(CardShape.DIAMOND, CardNumber.QUEEN), + new Card(CardShape.DIAMOND, CardNumber.ACE), new Card(CardShape.DIAMOND, CardNumber.TWO))); + + mockDeck = new Deck(cardList); + realDeck = new Deck(); + } + + @Test + @DisplayName("카드 덱은 52장의 카드로 이루어져 있다.") + void deckSizeTest() { + assertThat(realDeck.getCards().size()).isEqualTo(52); + } + + @Test + @DisplayName("맨 위의 카드를 한 장 뽑는다.") + void drawTest() { + assertThat(mockDeck.draw()).isEqualTo(new Card(CardShape.HEART, CardNumber.KING)); + assertThat(mockDeck.draw()).isEqualTo(new Card(CardShape.HEART, CardNumber.QUEEN)); + assertThat(mockDeck.draw()).isEqualTo(new Card(CardShape.HEART, CardNumber.ACE)); + } +} diff --git a/src/test/java/blackjack/domain/game/GameAccountTest.java b/src/test/java/blackjack/domain/game/GameAccountTest.java new file mode 100644 index 00000000000..d8f7b190ddf --- /dev/null +++ b/src/test/java/blackjack/domain/game/GameAccountTest.java @@ -0,0 +1,75 @@ +package blackjack.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Player; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class GameAccountTest { + + private GameAccount gameAccount; + private Player player; + private Money money; + + @BeforeEach + void setUp() { + gameAccount = new GameAccount(); + player = new Player("loki"); + money = new Money(50000); + } + + @AfterEach + void afterEach() { + gameAccount.clearStore(); + } + + @Test + @DisplayName("플레이어가 금액을 배팅한다.") + void betMoneyTest() { + gameAccount.betMoney(player, money); + + assertThat(gameAccount.findMoney(player)).isEqualTo(new Money(50000)); + } + + @ParameterizedTest + @MethodSource("provideGameResultsAndMoney") + @DisplayName("플레이어의 게임 결과를 배팅한 금액에 적용한다") + void applyGameResultsTest(GameResult gameResult, Money expectedMoney) { + Map gameResults = Map.of(player, gameResult); + + gameAccount.betMoney(player, money); + gameAccount.applyGameResults(gameResults); + + assertThat(gameAccount.findMoney(player)).isEqualTo(expectedMoney); + } + + private static Stream provideGameResultsAndMoney() { + return Stream.of( + Arguments.of(GameResult.BLACKJACK_WIN, new Money(75000)), + Arguments.of(GameResult.WIN, new Money(50000)), + Arguments.of(GameResult.DRAW, new Money(0)), + Arguments.of(GameResult.LOSE, new Money(-50000) + )); + } + + + @Test + @DisplayName("플레이어의 배팅 금액 결과로 딜러의 수익을 계산한다.") + void calculateDealerIncome() { + gameAccount.betMoney(player, money); + gameAccount.betMoney(new Player("jazz"), new Money(-20000)); + + Money dealerIncome = gameAccount.calculateDealerIncome(); + + assertThat(dealerIncome).isEqualTo(new Money(-30000)); + } +} diff --git a/src/test/java/blackjack/domain/gamer/DealerTest.java b/src/test/java/blackjack/domain/gamer/DealerTest.java new file mode 100644 index 00000000000..9c98fa3db06 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/DealerTest.java @@ -0,0 +1,39 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; + +class DealerTest { + + Dealer dealer; + + @BeforeEach + void setUP() { + dealer = new Dealer(); + } + + @Test + @DisplayName("카드의 총합이 16 이하이면 카드를 받을 수 있다.") + void receiveCardTest() { + dealer.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + dealer.addCard(new Card(CardShape.HEART, CardNumber.SIX)); + + assertThat(dealer.canReceiveCard()).isTrue(); + } + + @Test + @DisplayName("카드의 총합이 16을 초과하면 카드를 받을 수 없다.") + void cantReceiveCardTest() { + dealer.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + dealer.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(dealer.canReceiveCard()).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/gamer/HandTest.java b/src/test/java/blackjack/domain/gamer/HandTest.java new file mode 100644 index 00000000000..63b7730d349 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/HandTest.java @@ -0,0 +1,39 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HandTest { + + private Hand hand; + + @BeforeEach + void setUp() { + hand = new Hand(List.of( + new Card(CardShape.CLOVER, CardNumber.KING), + new Card(CardShape.HEART, CardNumber.FIVE) + )); + } + + @Test + @DisplayName("핸드의 첫 번째 카드를 반환한다") + void getFirstCardTest() { + Card firstCard = hand.getFirstCard(); + + assertThat(firstCard).isEqualTo(new Card(CardShape.CLOVER, CardNumber.KING)); + } + + @Test + @DisplayName("핸드의 밸류를 정상적으로 생성한다.") + void createHandValueTest() { + assertThatNoException().isThrownBy(() -> hand.generateValue()); + } +} diff --git a/src/test/java/blackjack/domain/gamer/HandValueTest.java b/src/test/java/blackjack/domain/gamer/HandValueTest.java new file mode 100644 index 00000000000..8dfee3ed2e9 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/HandValueTest.java @@ -0,0 +1,140 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class HandValueTest { + + @Nested + @DisplayName("핸드 점수 계산 테스트") + class CalculateScoreTest { + + @Test + @DisplayName("ACE가 없을때의 숫자 합을 계산한다.") + void sumWithoutAceTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.CLOVER, CardNumber.KING), + new Card(CardShape.HEART, CardNumber.FIVE) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.getScore()).isEqualTo(15); + } + + @Test + @DisplayName("핸드 점수 합이 21을 초과하는 경우, ACE를 1로 계산한다.") + void sumWithOneAceTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.KING), + new Card(CardShape.DIAMOND, CardNumber.KING) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.getScore()).isEqualTo(21); + } + + @Test + @DisplayName("핸드 점수 합이 21을 초과하지 않으면, ACE를 11로 계산한다.") + void sumWithElevenAceTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.KING) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.getScore()).isEqualTo(21); + } + } + + @Nested + @DisplayName("블랙잭 테스트") + class BlackjackTest { + + @Test + @DisplayName("카드가 2장이면서 핸드의 점수 합이 21점이면 블랙잭이다.") + void isBlackjackTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.KING) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.isBlackjack()).isTrue(); + } + + @Test + @DisplayName("카드가 2장이 아니면 블랙잭이 아니다") + void isNotBlackjackTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.JACK), + new Card(CardShape.CLOVER, CardNumber.NINE), + new Card(CardShape.HEART, CardNumber.TWO) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.isBlackjack()).isFalse(); + } + + @Test + @DisplayName("핸드 점수 합이 21점이 아니면 블랙잭이 아니다") + void isNotBlackjackTest2() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.JACK), + new Card(CardShape.CLOVER, CardNumber.NINE) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.isBlackjack()).isFalse(); + } + } + + @Nested + @DisplayName("버스트 테스트") + class BustTest { + + @Test + @DisplayName("핸드 점수 합이 21점을 초과하면 버스트다.") + void isBustTest() { + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.FIVE), + new Card(CardShape.CLOVER, CardNumber.JACK), + new Card(CardShape.DIAMOND, CardNumber.NINE) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.isBust()).isTrue(); + } + + @Test + @DisplayName("핸드 점수 합이 21점을 초과하지 않으면 버스트가 아니다.") + void isNotBustTest() { + + List cards = new ArrayList<>(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.DIAMOND, CardNumber.NINE) + )); + + HandValue handValue = new HandValue(cards); + + assertThat(handValue.isBust()).isFalse(); + } + } +} diff --git a/src/test/java/blackjack/domain/gamer/NameTest.java b/src/test/java/blackjack/domain/gamer/NameTest.java new file mode 100644 index 00000000000..453eeac6d21 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/NameTest.java @@ -0,0 +1,26 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class NameTest { + + @ParameterizedTest + @ValueSource(strings = {"a", "abcde"}) + @DisplayName("이름의 길이가 1글자에서 5글자인 경우 예외가 발생되지 않는다.") + void validNameLengthTest(String name) { + assertThatCode(() -> new Name(name)).doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = {"", "abcdef"}) + @DisplayName("이름의 길이가 0글자이거나, 5글자를 초과하면 예외가 발생된다.") + void invalidNameLengthTest(String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름의 길이는 최소 1글자부터 최대 5글자까지 가능합니다."); + } +} diff --git a/src/test/java/blackjack/domain/gamer/PlayerTest.java b/src/test/java/blackjack/domain/gamer/PlayerTest.java new file mode 100644 index 00000000000..e55e45b1921 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/PlayerTest.java @@ -0,0 +1,155 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PlayerTest { + + private Hand score14Hand, score21Hand, bustHand, blackjackHand; + + @BeforeEach + void setUP() { + score14Hand = new Hand(List.of( + new Card(CardShape.DIAMOND, CardNumber.KING), new Card(CardShape.SPADE, CardNumber.FOUR) + )); + + score21Hand = new Hand(List.of( + new Card(CardShape.CLOVER, CardNumber.KING), new Card(CardShape.SPADE, CardNumber.NINE), + new Card(CardShape.SPADE, CardNumber.TWO) + )); + + bustHand = new Hand(List.of( + new Card(CardShape.HEART, CardNumber.KING), new Card(CardShape.SPADE, CardNumber.JACK), + new Card(CardShape.SPADE, CardNumber.TWO) + )); + + blackjackHand = new Hand(List.of( + new Card(CardShape.SPADE, CardNumber.ACE), new Card(CardShape.SPADE, CardNumber.QUEEN) + )); + } + + @Nested + @DisplayName("카드를 받는 조건 테스트") + class CanReceiveCardTest { + + @Test + @DisplayName("카드의 총합이 21 이하이면 카드를 받을 수 있다.") + void receiveCardTest() { + Player player = new Player(score14Hand, new Name("hogi")); + + assertThat(player.canReceiveCard()).isTrue(); + } + + @Test + @DisplayName("카드의 총합이 21을 초과하면 카드를 받을 수 없다.") + void cantReceiveCardTest() { + Player player = new Player(bustHand, new Name("jazz")); + + assertThat(player.canReceiveCard()).isFalse(); + } + } + + @Nested + @DisplayName("플레이어가 패배하는 경우의 수 테스트") + class PlayerLoseTest { + + @Test + @DisplayName("점수가 21을 초과하면 딜러의 점수와 무관하게 패배한다") + void playerBustTest() { + Player player = new Player(bustHand, new Name("ready")); + HandValue dealerHandValue = new HandValue(22, 3); + + GameResult gameResult = player.compete(dealerHandValue); + + assertThat(gameResult).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하이면서, 딜러의 점수보다 낮으면 패배한다.") + void playerHandValueLowerThanDealer() { + Player player = new Player(score14Hand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(21, 3); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("딜러가 블랙잭일 때, 플레이어가 블랙잭이 아니면 패배한다.") + void playerLoseWhenDealerBlackjack() { + Player player = new Player(score14Hand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(21, 2); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.LOSE); + } + } + + @Nested + @DisplayName("플레이어가 승리하는 경우의 수 테스트") + class PlayerWinTest { + + @Test + @DisplayName("플레이어 점수가 21 이하일 때, 딜러의 점수가 21을 초과하면 승리한다.") + void dealerBustTest() { + Player player = new Player(score21Hand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(22, 4); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.WIN); + } + + @Test + @DisplayName("플레이어 점수가 21 이하이고, 딜러의 점수보다 큰 경우 승리한다.") + void playerHandValueHigherThanDealer() { + Player player = new Player(score21Hand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(14, 2); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.WIN); + } + } + + @Nested + @DisplayName("플레이어가 블랙잭 승리하는 경우의 수 테스트") + class PlayerBlackjackWinTest { + + @Test + @DisplayName("플레이어가 블랙잭일 때, 딜러가 블랙잭이 아니면 블랙잭 승리한다.") + void dealerBustTest() { + Player player = new Player(blackjackHand, new Name("jazz")); + HandValue dealerHandValue21 = new HandValue(21, 3); + HandValue dealerBustHand = new HandValue(22, 3); + + assertThat(player.compete(dealerHandValue21)).isEqualTo(GameResult.BLACKJACK_WIN); + assertThat(player.compete(dealerBustHand)).isEqualTo(GameResult.BLACKJACK_WIN); + } + } + + @Nested + @DisplayName("플레이어 딜러 무승부 테스트") + class PlayerDealerDrawTest { + + @Test + @DisplayName("플레이어가 블랙잭일 때, 딜러가 블랙잭이면 무승부.") + void allBlackjackTest() { + Player player = new Player(blackjackHand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(21, 2); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.DRAW); + } + + @Test + @DisplayName("플레이어가 블랙잭일 때, 딜러가 블랙잭이면 무승부.") + void tieScoreTest() { + Player player = new Player(score21Hand, new Name("jazz")); + HandValue dealerHandValue = new HandValue(21, 4); + + assertThat(player.compete(dealerHandValue)).isEqualTo(GameResult.DRAW); + } + } +} diff --git a/src/test/java/blackjack/domain/gamer/PlayersTest.java b/src/test/java/blackjack/domain/gamer/PlayersTest.java new file mode 100644 index 00000000000..06ecd10e521 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/PlayersTest.java @@ -0,0 +1,45 @@ +package blackjack.domain.gamer; + +import static java.util.List.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + @Test + @DisplayName("유효한 입력에 대해서는 예외가 발생되지 않는다.") + void validPlayerNamesTest() { + assertThatCode(() -> new Players(List.of("a", "b"))) + .doesNotThrowAnyException(); + assertThatCode(() -> new Players(List.of("a", "b", "c", "d", "e", "f", "g", "h"))) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("플레이어의 이름은 중복을 허용하지 않는다.") + void duplicatePlayerNamesTest() { + assertThatThrownBy(() -> new Players(of("hogi", "sang", "hogi"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("플레이어는 적어도 2명 이상이어야 한다.") + void minimumPlayerCountTest() { + assertThatThrownBy(() -> new Players(of("a"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("플레이어는 최소 2명에서 최대 8명까지 가능합니다"); + + } + + @Test + @DisplayName("플레이어는 최대 8명까지만 가능하다.") + void maximumPlayerCountTest() { + assertThatThrownBy(() -> new Players(of("a", "b", "c", "d", "e", "f", "g", "h", "g"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 중복될 수 없습니다."); + } +}