diff --git a/README.md b/README.md index 556099c4de..e432c782c7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,41 @@ 블랙잭 미션 저장소 -## 우아한테크코스 코드리뷰 +## 기능 요구 사항 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +### 게임 + +- [x] 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 쪽이 승리한다. +- [x] 게임을 시작하면 플레이어는 두 장의 카드를 지급 받는다. + - [x] 이후 플레이어와 딜러의 카드를 출력한다. + - [x] 단, 딜러의 카드는 하나만 출력한다. +- [x] 플레이어는 카드의 숫자 합이 21을 초과하지 않는다면 카드를 원하는 만큼 다시 뽑을 수 있다. + - [x] 새로 받을 때 마다 해당 플레이어의 카드를 출력한다. +- [x] 플레이어가 카드를 다 받으면 딜러의 카드를 확인한다. +- [x] 딜러의 카드 합이 17 이상이 될 때 까지 카드를 받는다. +- [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] 플레이어의 점수가 21 이하이고, 딜러와 동점인 경우 패배한다. + - [x] 플레이어의 점수가 21 이하이고, 딜러의 점수가 21을 초과하면 승리한다. + - [x] 플레이어와 딜러의 점수가 모두 21 이하이고, 딜러의 점수보다 크면 승리한다. diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 0000000000..2ddbf1df98 --- /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 0000000000..8b5d60de45 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,118 @@ +package blackjack.controller; + +import java.util.List; + +import blackjack.domain.BlackjackConstants; +import blackjack.domain.card.Deck; +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.Player; +import blackjack.domain.gamer.Players; +import blackjack.dto.DealerInitialHandDto; +import blackjack.dto.DealerResultDto; +import blackjack.dto.GamerHandDto; +import blackjack.dto.PlayerResultsDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class BlackjackController { + + private static final String HIT_COMMAND = "y"; + private static final String STAND_COMMAND = "n"; + + private final InputView inputView; + private final OutputView outputView; + + public BlackjackController() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + } + + public void run() { + Players players = getPlayers(); + Dealer dealer = new Dealer(); + Deck deck = new Deck(); + deck.shuffle(); + outputView.printEmptyLine(); + + setUpInitialHands(players, deck, dealer); + distributeCardToPlayers(players, deck); + distributeCardToDealer(dealer, deck); + printAllGamerScores(dealer, players); + printResult(dealer, players); + } + + private Players getPlayers() { + List playerNames = inputView.receivePlayerNames(); + + return new Players(playerNames); + } + + private void setUpInitialHands(Players players, Deck deck, Dealer dealer) { + players.initAllPlayersCard(deck, BlackjackConstants.INITIAL_CARD_COUNT.getValue()); + dealer.initCard(deck, BlackjackConstants.INITIAL_CARD_COUNT.getValue()); + printInitialHands(players, dealer); + } + + private void printInitialHands(Players players, Dealer dealer) { + DealerInitialHandDto dealerInitialHandDto = DealerInitialHandDto.fromDealer(dealer); + List playerInitialHandDto = players.getPlayers().stream() + .map(GamerHandDto::fromGamer) + .toList(); + + outputView.printInitialHands(dealerInitialHandDto, playerInitialHandDto); + outputView.printEmptyLine(); + } + + private void distributeCardToPlayers(Players players, Deck deck) { + for (Player player : players.getPlayers()) { + distributeCardToPlayer(deck, player); + } + } + + private void distributeCardToPlayer(Deck deck, Player player) { + while (canDistribute(player)) { + player.addCard(deck.draw()); + outputView.printGamerNameAndHand(GamerHandDto.fromGamer(player)); + } + outputView.printEmptyLine(); + } + + private boolean canDistribute(Player player) { + return player.canReceiveCard() && HIT_COMMAND.equals(getCommand(player)); + } + + private String getCommand(Player player) { + String command = inputView.receiveCommand(player.getName().value()); + if (HIT_COMMAND.equals(command) || STAND_COMMAND.equals(command)) { + return command; + } + throw new IllegalArgumentException(HIT_COMMAND + " 또는 " + STAND_COMMAND + "만 입력 가능합니다."); + } + + private void distributeCardToDealer(Dealer dealer, Deck deck) { + while (dealer.canReceiveCard()) { + dealer.addCard(deck.draw()); + outputView.printDealerMessage(dealer.getName().value()); + } + outputView.printEmptyLine(); + } + + private void printAllGamerScores(Dealer dealer, Players players) { + outputView.printScore(GamerHandDto.fromGamer(dealer), dealer.getScore()); + printPlayersScores(players); + outputView.printEmptyLine(); + } + + private void printPlayersScores(Players players) { + players.getPlayers().forEach(player -> outputView.printScore( + GamerHandDto.fromGamer(player), player.getScore() + )); + } + + private void printResult(Dealer dealer, Players players) { + PlayerResultsDto playerResultsDto = PlayerResultsDto.ofPlayersAndDealerScore(players, dealer); + DealerResultDto dealerResultDto = DealerResultDto.ofDealerAndPlayers(dealer, players); + + outputView.printFinalResult(dealerResultDto, playerResultsDto); + } +} diff --git a/src/main/java/blackjack/domain/BlackjackConstants.java b/src/main/java/blackjack/domain/BlackjackConstants.java new file mode 100644 index 0000000000..9e2833dab1 --- /dev/null +++ b/src/main/java/blackjack/domain/BlackjackConstants.java @@ -0,0 +1,17 @@ +package blackjack.domain; + +public enum BlackjackConstants { + BLACKJACK_VALUE(21), + DEALER_MINIMUM_VALUE(17), + INITIAL_CARD_COUNT(2); + + private final int value; + + BlackjackConstants(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/blackjack/domain/card/Card.java b/src/main/java/blackjack/domain/card/Card.java new file mode 100644 index 0000000000..b2368850f5 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,12 @@ +package blackjack.domain.card; + +public record Card(CardShape cardShape, CardNumber cardNumber) { + + public boolean isAce() { + return cardNumber == CardNumber.ACE; + } + + public int getNumber() { + return cardNumber.getValue(); + } +} 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 0000000000..abaa843383 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardNumber.java @@ -0,0 +1,34 @@ +package blackjack.domain.card; + +public enum CardNumber { + + ACE("A", 11), + TWO("2", 2), + THREE("3", 3), + FOUR("4", 4), + FIVE("5", 5), + SIX("6", 6), + SEVEN("7", 7), + EIGHT("8", 8), + NINE("9", 9), + TEN("10", 10), + JACK("J", 10), + QUEEN("Q", 10), + KING("K", 10); + + private final String name; + private final int value; + + CardNumber(String name, int value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + 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 0000000000..a2810cd62b --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardShape.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +public enum CardShape { + + HEART("하트"), + CLOVER("클로버"), + SPADE("스페이드"), + DIAMOND("다이아몬드"); + + private final String name; + + CardShape(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} 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 0000000000..e7582663ec --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,40 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Deck { + + private final List cards; + + public Deck() { + this.cards = initAllCards(); + } + + private List initAllCards() { + return Arrays.stream(CardShape.values()) + .flatMap(cardShape -> Arrays.stream(CardNumber.values()) + .map(number -> new Card(cardShape, number))) + .collect(Collectors.toCollection(ArrayList::new)); + } + + public void shuffle() { + Collections.shuffle(cards); + } + + public Card draw() { + if (cards.isEmpty()) { + cards.addAll(initAllCards()); + shuffle(); + } + + return cards.remove(0); + } + + public List getCards() { + return new ArrayList<>(cards); + } +} 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 0000000000..baed0ea946 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/BlackjackGamer.java @@ -0,0 +1,53 @@ +package blackjack.domain.gamer; + +import java.util.ArrayList; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; + +public abstract class BlackjackGamer { + + private final Name name; + private final Hand hand; + + public BlackjackGamer(Name name) { + this.name = name; + this.hand = new Hand(new ArrayList<>()); + } + + public abstract boolean canReceiveCard(); + + public void initCard(Deck deck, int initialCardCount) { + for (int i = 0; i < initialCardCount; i++) { + addCard(deck.draw()); + } + } + + public void addCard(Card card) { + hand.add(card); + } + + public boolean isBust() { + return hand.checkIfBust(); + } + + public boolean isBlackJack() { + return hand.checkIfBlackjack(); + } + + public Card getFirstCard() { + return hand.getFirstCard(); + } + + public int getScore() { + return hand.calculateScore(); + } + + public Name getName() { + return name; + } + + public Hand getHand() { + return hand; + } +} 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 0000000000..303d8772ca --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Dealer.java @@ -0,0 +1,31 @@ +package blackjack.domain.gamer; + +import java.util.List; + +import blackjack.domain.BlackjackConstants; + +public class Dealer extends BlackjackGamer { + + private static final String DEFAULT_DEALER_NAME = "딜러"; + + public Dealer() { + super(new Name(DEFAULT_DEALER_NAME)); + } + + @Override + public boolean canReceiveCard() { + return getScore() < BlackjackConstants.DEALER_MINIMUM_VALUE.getValue(); + } + + public int calculateWinCount(List playerResults) { + return (int)playerResults.stream() + .filter(GameResult::isLose) + .count(); + } + + public int calculateLoseCount(List playerResults) { + return (int)playerResults.stream() + .filter(GameResult::isWin) + .count(); + } +} 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 0000000000..7c756ffafb --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/GameResult.java @@ -0,0 +1,25 @@ +package blackjack.domain.gamer; + +public enum GameResult { + + WIN("승"), + LOSE("패"); + + private final String name; + + GameResult(String name) { + this.name = name; + } + + public boolean isWin() { + return this == WIN; + } + + public boolean isLose() { + return this == LOSE; + } + + public String getName() { + return name; + } +} 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 0000000000..1ac9422b13 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Hand.java @@ -0,0 +1,66 @@ +package blackjack.domain.gamer; + +import java.util.List; + +import blackjack.domain.BlackjackConstants; +import blackjack.domain.card.Card; + +public record Hand(List cards) { + + private static final int ACE_VALUE_MODIFIER = 10; + + public void add(Card card) { + cards.add(card); + } + + public int calculateScore() { + int sum = cards.stream() + .mapToInt(Card::getNumber) + .sum(); + + if (hasAce()) { + return adjustSumWithAce(sum); + } + return sum; + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + /** + * 21 이하이면서 가장 큰 값을 찾습니다. + * Ace는 CardNumber에 11로 지정하였기에, 합계가 21을 초과하면 Ace의 개수만큼 10씩 빼는 방법으로 Ace를 1로 계산합니다. + */ + private int adjustSumWithAce(int sum) { + int aceCount = (int)cards.stream() + .filter(Card::isAce) + .count(); + + for (int i = 0; i < aceCount; i++) { + sum = adjust(sum); + } + return sum; + } + + private int adjust(int sum) { + if (sum > BlackjackConstants.BLACKJACK_VALUE.getValue()) { + sum -= ACE_VALUE_MODIFIER; + } + return sum; + } + + public boolean checkIfBust() { + return calculateScore() > BlackjackConstants.BLACKJACK_VALUE.getValue(); + } + + public boolean checkIfBlackjack() { + return cards.size() == BlackjackConstants.INITIAL_CARD_COUNT.getValue() + && calculateScore() == BlackjackConstants.BLACKJACK_VALUE.getValue(); + } + + public Card getFirstCard() { + return cards.get(0); + } +} 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 0000000000..2974903b49 --- /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 0000000000..5a3e6d34a5 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Player.java @@ -0,0 +1,38 @@ +package blackjack.domain.gamer; + +import blackjack.domain.BlackjackConstants; + +public class Player extends BlackjackGamer { + + public Player(Name name) { + super(name); + } + + @Override + public boolean canReceiveCard() { + return getScore() <= BlackjackConstants.BLACKJACK_VALUE.getValue(); + } + + public GameResult getGameResult(Dealer dealer) { + if (isBust()) { + return GameResult.LOSE; + } + if (dealer.isBust()) { + return GameResult.WIN; + } + if (dealer.isBlackJack()) { + return GameResult.LOSE; + } + if (isBlackJack()) { + return GameResult.WIN; + } + return compareScore(dealer.getScore(), getScore()); + } + + private GameResult compareScore(int dealerScore, int playerScore) { + if (dealerScore >= playerScore) { + return GameResult.LOSE; + } + return GameResult.WIN; + } +} 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 0000000000..ce71fe3e68 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Players.java @@ -0,0 +1,58 @@ +package blackjack.domain.gamer; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import blackjack.domain.card.Deck; + +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(name -> new Player(new Name(name))) + .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, int initialCardCount) { + players.forEach(player -> player.initCard(deck, initialCardCount)); + } + + public List getPlayers() { + return List.copyOf(players); + } + + public List getAllPlayerGameResults(Dealer dealer) { + return players.stream() + .map(player -> player.getGameResult(dealer)) + .toList(); + } +} diff --git a/src/main/java/blackjack/dto/CardDto.java b/src/main/java/blackjack/dto/CardDto.java new file mode 100644 index 0000000000..f58c908d00 --- /dev/null +++ b/src/main/java/blackjack/dto/CardDto.java @@ -0,0 +1,13 @@ +package blackjack.dto; + +import blackjack.domain.card.Card; + +public record CardDto(String cardNumber, String cardShape) { + + public static CardDto fromCard(Card card) { + String name = card.cardNumber().getName(); + String shape = card.cardShape().getName(); + + return new CardDto(name, shape); + } +} diff --git a/src/main/java/blackjack/dto/DealerInitialHandDto.java b/src/main/java/blackjack/dto/DealerInitialHandDto.java new file mode 100644 index 0000000000..57fa48e6e8 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerInitialHandDto.java @@ -0,0 +1,14 @@ +package blackjack.dto; + +import blackjack.domain.card.Card; +import blackjack.domain.gamer.Dealer; + +public record DealerInitialHandDto(String name, CardDto firstCard) { + + public static DealerInitialHandDto fromDealer(Dealer dealer) { + String dealerName = dealer.getName().value(); + Card dealerFirstCard = dealer.getFirstCard(); + + return new DealerInitialHandDto(dealerName, CardDto.fromCard(dealerFirstCard)); + } +} diff --git a/src/main/java/blackjack/dto/DealerResultDto.java b/src/main/java/blackjack/dto/DealerResultDto.java new file mode 100644 index 0000000000..6c7cf417b2 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerResultDto.java @@ -0,0 +1,19 @@ +package blackjack.dto; + +import java.util.List; + +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Players; + +public record DealerResultDto(String name, int winCount, int loseCount) { + + public static DealerResultDto ofDealerAndPlayers(Dealer dealer, Players players) { + String dealerName = dealer.getName().value(); + List allPlayerGameResults = players.getAllPlayerGameResults(dealer); + int dealerWinCount = dealer.calculateWinCount(allPlayerGameResults); + int dealerLoseCount = dealer.calculateLoseCount(allPlayerGameResults); + + return new DealerResultDto(dealerName, dealerWinCount, dealerLoseCount); + } +} diff --git a/src/main/java/blackjack/dto/GamerHandDto.java b/src/main/java/blackjack/dto/GamerHandDto.java new file mode 100644 index 0000000000..31f053b5f0 --- /dev/null +++ b/src/main/java/blackjack/dto/GamerHandDto.java @@ -0,0 +1,17 @@ +package blackjack.dto; + +import java.util.List; + +import blackjack.domain.gamer.BlackjackGamer; + +public record GamerHandDto(String name, List gamerHand) { + + public static GamerHandDto fromGamer(BlackjackGamer gamer) { + String gamerName = gamer.getName().value(); + List cards = gamer.getHand().cards().stream() + .map(CardDto::fromCard) + .toList(); + + return new GamerHandDto(gamerName, cards); + } +} diff --git a/src/main/java/blackjack/dto/PlayerResultsDto.java b/src/main/java/blackjack/dto/PlayerResultsDto.java new file mode 100644 index 0000000000..71dd818ce3 --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerResultsDto.java @@ -0,0 +1,21 @@ +package blackjack.dto; + +import java.util.LinkedHashMap; +import java.util.Map; + +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.Players; + +public record PlayerResultsDto(Map playerNameResults) { + + public static PlayerResultsDto ofPlayersAndDealerScore(Players players, Dealer dealer) { + Map nameResults = new LinkedHashMap<>(); + + players.getPlayers().forEach(player -> nameResults.put( + player.getName().value(), + player.getGameResult(dealer).getName() + )); + + return new PlayerResultsDto(nameResults); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..606f918259 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,24 @@ +package blackjack.view; + +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 String receiveCommand(String name) { + System.out.println(name + "는(은) 한장의 카드를 더 받겠습니까?(예는 y, 아니오: n)"); + 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..472574d793 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,123 @@ +package blackjack.view; + +import java.util.List; + +import blackjack.dto.CardDto; +import blackjack.dto.DealerInitialHandDto; +import blackjack.dto.DealerResultDto; +import blackjack.dto.GamerHandDto; +import blackjack.dto.PlayerResultsDto; + +public class OutputView { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String COLON_WITH_SPACE = ": "; + + public void printInitialHands(DealerInitialHandDto dealerInitialHandDto, List playersInitialHandDto) { + printInitialMessage(dealerInitialHandDto, playersInitialHandDto); + printDealerFirstCard(dealerInitialHandDto); + printAllPlayerHands(playersInitialHandDto); + } + + private void printInitialMessage( + DealerInitialHandDto dealerInitialHandDto, + List playersInitialHandDto + ) { + System.out.printf("%s와 %s에게 2장을 나누었습니다." + LINE_SEPARATOR, + dealerInitialHandDto.name(), joinNames(getPlayerNames(playersInitialHandDto)) + ); + } + + private String joinNames(List names) { + return String.join(", ", names); + } + + private List getPlayerNames(List playersInitialHandDto) { + return playersInitialHandDto.stream().map(GamerHandDto::name).toList(); + } + + private void printDealerFirstCard(DealerInitialHandDto dealerInitialHandDto) { + System.out.println(buildNameAndCard( + dealerInitialHandDto.name(), formatCardName(dealerInitialHandDto.firstCard()) + )); + } + + private String formatCardName(CardDto cardDto) { + return cardDto.cardNumber() + cardDto.cardShape(); + } + + private void printAllPlayerHands(List playersInitialHandDto) { + playersInitialHandDto.forEach(this::printGamerNameAndHand); + } + + public void printGamerNameAndHand(GamerHandDto gamerHandDto) { + System.out.println(buildGamerNameAndHand(gamerHandDto)); + } + + private StringBuilder buildGamerNameAndHand(GamerHandDto gamerHandDto) { + return buildNameAndCard(gamerHandDto.name(), joinNames(getCardNames(gamerHandDto))); + } + + private StringBuilder buildNameAndCard(String name, String card) { + return new StringBuilder() + .append(name) + .append(" ") + .append("카드") + .append(COLON_WITH_SPACE) + .append(card); + } + + private List getCardNames(GamerHandDto gamerHandDto) { + return gamerHandDto.gamerHand().stream() + .map(this::formatCardName) + .toList(); + } + + public void printDealerMessage(String dealerName) { + System.out.printf("%s는 16이하라 한장의 카드를 더 받았습니다." + LINE_SEPARATOR, dealerName); + } + + public void printScore(GamerHandDto gamerHandDto, int score) { + StringBuilder builder = buildGamerNameAndHand(gamerHandDto) + .append(" - ") + .append("결과") + .append(COLON_WITH_SPACE) + .append(score); + System.out.println(builder); + } + + public void printFinalResult(DealerResultDto dealerResultDto, PlayerResultsDto playerResultsDto) { + System.out.println("## 최종 승패"); + printDealerResult(dealerResultDto); + printPlayerResults(playerResultsDto); + } + + private void printDealerResult(DealerResultDto dealerResultDto) { + int winCount = dealerResultDto.winCount(); + int loseCount = dealerResultDto.loseCount(); + printResult(dealerResultDto.name(), formatDealerResult(winCount, loseCount)); + } + + private void printResult(String name, String result) { + System.out.println(name + COLON_WITH_SPACE + result); + } + + private String formatDealerResult(int winCount, int loseCount) { + StringBuilder stringBuilder = new StringBuilder(); + if (winCount > 0) { + stringBuilder.append(winCount).append("승 "); + } + if (loseCount > 0) { + stringBuilder.append(loseCount).append("패"); + } + return stringBuilder.toString(); + } + + private void printPlayerResults(PlayerResultsDto playerResultsDto) { + playerResultsDto.playerNameResults().forEach(this::printResult); + } + + public void printEmptyLine() { + System.out.println(); + } +} diff --git a/src/test/java/blackjack/domain/card/CardTest.java b/src/test/java/blackjack/domain/card/CardTest.java new file mode 100644 index 0000000000..369534ac05 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,18 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardTest { + + @Test + @DisplayName("카드의 모양과 숫자가 같으면 같은 카드로 판단한다.") + void equalsTest() { + Card card1 = new Card(CardShape.DIAMOND, CardNumber.ACE); + Card card2 = new Card(CardShape.DIAMOND, CardNumber.ACE); + + assertThat(card1.equals(card2)).isTrue(); + } +} 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 0000000000..a242603d36 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,43 @@ +package blackjack.domain.card; + +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; + +public class DeckTest { + + Deck deck; + + @BeforeEach + void setUP() { + deck = new Deck(); + } + + @Test + @DisplayName("카드 덱은 52장의 카드로 이루어져 있다.") + void deckSizeTest() { + assertThat(deck.getCards().size()).isEqualTo(52); + } + + @Test + @DisplayName("맨 위의 카드를 한 장 뽑는다.") + void drawTest() { + assertThat(deck.draw()).isEqualTo(new Card(CardShape.HEART, CardNumber.ACE)); + } + + @Test + @DisplayName("52장의 카드를 모두 뽑으면, 새로 52장의 카드를 세팅한다.") + void addAfterDrawTest() { + // 52장의 카드를 모두 뽑는다. + for (int i = 0; i < 52; i++) { + deck.draw(); + } + + // 52장을 다 뽑고, 한 장을 더 뽑으면 뽑기 전에 패를 초기화한다. + deck.draw(); + + assertThat(deck.getCards().size()).isEqualTo(51); + } +} 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 0000000000..98c9ccbb9a --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/DealerTest.java @@ -0,0 +1,66 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.*; + +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; + +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(); + } + + @Nested + @DisplayName("딜러의 승,패 횟수를 계산한다.") + class DealerResultTest { + + List playerResults; + + @BeforeEach + void setUP() { + playerResults = List.of(GameResult.WIN, GameResult.LOSE, GameResult.WIN, GameResult.LOSE, GameResult.WIN); + } + + @Test + @DisplayName("딜러의 승리 횟수를 올바르게 계산한다.") + void calculateDealerWinCountTest() { + assertThat(dealer.calculateWinCount(playerResults)).isEqualTo(2); + } + + @Test + @DisplayName("딜러의 패배 횟수를 올바르게 계산한다.") + void calculateDealerLoseCountTest() { + assertThat(dealer.calculateLoseCount(playerResults)).isEqualTo(3); + } + } +} 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 0000000000..9c2bbf6ea4 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/HandTest.java @@ -0,0 +1,51 @@ +package blackjack.domain.gamer; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 HandTest { + + Hand hand; + + @Test + @DisplayName("ACE가 없을때의 숫자 합을 계산한다.") + void sumWithoutAceTest() { + hand = new Hand(List.of( + new Card(CardShape.CLOVER, CardNumber.KING), + new Card(CardShape.HEART, CardNumber.FIVE) + )); + + assertThat(hand.calculateScore()).isEqualTo(15); + } + + @Test + @DisplayName("숫자의 합이 21을 초과하는 경우, ACE를 1로 계산한다.") + void sumWithOneAceTest() { + hand = new Hand(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.KING), + new Card(CardShape.DIAMOND, CardNumber.KING) + )); + + assertThat(hand.calculateScore()).isEqualTo(21); + } + + @Test + @DisplayName("숫자의 합이 21을 초과하지 않으면, ACE를 11로 계산한다.") + void sumWithElevenAceTest() { + hand = new Hand(List.of( + new Card(CardShape.HEART, CardNumber.ACE), + new Card(CardShape.CLOVER, CardNumber.KING) + )); + + assertThat(hand.calculateScore()).isEqualTo(21); + } +} 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 0000000000..453eeac6d2 --- /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 0000000000..b4587d61b4 --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/PlayerTest.java @@ -0,0 +1,139 @@ +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.Nested; +import org.junit.jupiter.api.Test; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardNumber; +import blackjack.domain.card.CardShape; + +class PlayerTest { + + Player player; + Dealer dealerWithBust; + Dealer dealerWithBlackjack; + Dealer dealerWith21Score; + Dealer dealerWith17Score; + + @BeforeEach + void setUP() { + player = new Player(new Name("hogi")); + + dealerWithBust = new Dealer(); + dealerWithBust.addCard(new Card(CardShape.DIAMOND, CardNumber.KING)); + dealerWithBust.addCard(new Card(CardShape.CLOVER, CardNumber.QUEEN)); + dealerWithBust.addCard(new Card(CardShape.HEART, CardNumber.TWO)); + + dealerWithBlackjack = new Dealer(); + dealerWithBlackjack.addCard(new Card(CardShape.DIAMOND, CardNumber.ACE)); + dealerWithBlackjack.addCard(new Card(CardShape.DIAMOND, CardNumber.JACK)); + + dealerWith17Score = new Dealer(); + dealerWith17Score.addCard(new Card(CardShape.SPADE, CardNumber.QUEEN)); + dealerWith17Score.addCard(new Card(CardShape.SPADE, CardNumber.SEVEN)); + + dealerWith21Score = new Dealer(); + dealerWith21Score.addCard(new Card(CardShape.DIAMOND, CardNumber.EIGHT)); + dealerWith21Score.addCard(new Card(CardShape.DIAMOND, CardNumber.SEVEN)); + dealerWith21Score.addCard(new Card(CardShape.DIAMOND, CardNumber.SIX)); + } + + @Test + @DisplayName("카드의 총합이 21 이하이면 카드를 받을 수 있다.") + void receiveCardTest() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.FIVE)); + + assertThat(player.canReceiveCard()).isTrue(); + } + + @Test + @DisplayName("카드의 총합이 21을 초과하면 카드를 받을 수 없다.") + void cantReceiveCardTest() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.FIVE)); + player.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(player.canReceiveCard()).isFalse(); + } + + @Nested + @DisplayName("플레이어가 승리한다.") + class IsWinTest { + + @Test + @DisplayName("플레이어 점수가 21 이하이고, 딜러의 점수보다 큰 경우 승리한다.") + void getResult_WithComparingScore() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.FOUR)); + player.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(player.getGameResult(dealerWith17Score)).isEqualTo(GameResult.WIN); + } + + @Test + @DisplayName("플레이어 점수가 21 이하이고, 딜러의 점수가 21을 초과하면 승리한다.") + void getResult_WithDealerBust() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.FIVE)); + + assertThat(player.getGameResult(dealerWithBust)).isEqualTo(GameResult.WIN); + } + + @Test + @DisplayName("플레이어와 딜러가 모두 21이고, 플레이어만 블랙잭인 경우 승리한다.") + void getResult_WithPlayerOnlyBlackjack() { + player.addCard(new Card(CardShape.SPADE, CardNumber.ACE)); + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + + assertThat(player.getGameResult(dealerWith21Score)).isEqualTo(GameResult.WIN); + } + } + + @Nested + @DisplayName("플레이어가 패배한다.") + class LoseTest { + + @Test + @DisplayName("점수가 21을 초과하면 딜러의 점수와 무관하게 패배한다") + void getResult_WithPlayerBust() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.FIVE)); + player.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(player.getGameResult(dealerWith17Score)).isEqualTo(GameResult.LOSE); + assertThat(player.getGameResult(dealerWithBust)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하일 때, 딜러가 블랙잭이면 패배한다.") + void getResult_WithDealerOnlyBlackjack() { + player.addCard(new Card(CardShape.SPADE, CardNumber.ACE)); + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + + assertThat(player.getGameResult(dealerWithBlackjack)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하일 때, 딜러의 점수보다 낮으면 패배한다.") + void getResult_WithComparingScore() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.SIX)); + + assertThat(player.getGameResult(dealerWith17Score)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하이고 동점인 경우 플레이어가 패배한다.") + void getResult_WithSameScore() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(player.getGameResult(dealerWith17Score)).isEqualTo(GameResult.LOSE); + } + } +} 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 0000000000..06ecd10e52 --- /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("이름은 중복될 수 없습니다."); + } +}