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..feb65f55e8 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackjackController.java @@ -0,0 +1,111 @@ +package blackjack.controller; + +import blackjack.domain.card.Deck; +import blackjack.domain.gamer.Dealer; +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Name; +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.GamersHandDto; +import blackjack.dto.PlayerGameResultsDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import blackjack.view.object.Command; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class BlackjackController { + + 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(); + + 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); + dealer.initCard(deck); + printInitialHands(players, dealer); + } + + private void printInitialHands(Players players, Dealer dealer) { + DealerInitialHandDto dealerInitialHandDto = DealerInitialHandDto.fromDealer(dealer); + GamersHandDto playersInitialHandDto = GamersHandDto.fromPlayers(players); + + outputView.printInitialHands(dealerInitialHandDto, playersInitialHandDto); + } + + 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.printPlayerHand(GamerHandDto.fromBlackjackGamer(player)); + } + } + + private boolean canDistribute(Player player) { + return player.canReceiveCard() && Command.isHit(getCommand(player)); + } + + private Command getCommand(Player player) { + return inputView.receiveCommand(player.getName().value()); + } + + private void distributeCardToDealer(Dealer dealer, Deck deck) { + while (dealer.canReceiveCard()) { + dealer.addCard(deck.draw()); + outputView.printDealerMessage(dealer.getName().value()); + } + } + + private void printAllGamerScores(Dealer dealer, Players players) { + outputView.printEmptyLine(); + outputView.printScore(GamerHandDto.fromBlackjackGamer(dealer), dealer.getScore()); + printPlayersScores(players); + } + + private void printPlayersScores(Players players) { + for (Player player : players.getPlayers()) { + outputView.printScore(GamerHandDto.fromBlackjackGamer(player), player.getScore()); + } + } + + private void printResult(Dealer dealer, Players players) { + Map playerGameResults = players.collectPlayerGameResults(dealer.getScore()); + PlayerGameResultsDto playerGameResultsDto = PlayerGameResultsDto.fromPlayerGameResults(playerGameResults); + List playerResults = new ArrayList<>(playerGameResultsDto.resultMap().values()); + DealerResultDto dealerResultDto = DealerResultDto.fromPlayerResults(playerResults); + + outputView.printResult(dealerResultDto, 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 0000000000..8a649540af --- /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 0000000000..044a8bb42f --- /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 0000000000..35169f3e95 --- /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 0000000000..430a9aa9d3 --- /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; +import java.util.stream.Collectors; + +public class Deck { + + private final LinkedList cards; + + public Deck() { + this.cards = Arrays.stream(CardShape.values()) + .flatMap(cardShape -> Arrays.stream(CardNumber.values()) + .map(number -> new Card(cardShape, number))) + .collect(Collectors.toCollection(LinkedList::new)); + } + + public 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/gamer/BlackjackGamer.java b/src/main/java/blackjack/domain/gamer/BlackjackGamer.java new file mode 100644 index 0000000000..efab58e7fe --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/BlackjackGamer.java @@ -0,0 +1,47 @@ +package blackjack.domain.gamer; + +import java.util.ArrayList; +import java.util.List; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.dto.CardDto; +import blackjack.dto.GamerHandDto; + +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) { + addCard(deck.draw()); + addCard(deck.draw()); + } + + public void addCard(Card card) { + hand.add(card); + } + + 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..a69e016955 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Dealer.java @@ -0,0 +1,16 @@ +package blackjack.domain.gamer; + +public class Dealer extends BlackjackGamer { + + private static final String DEFAULT_DEALER_NAME = "딜러"; + private static final int DEALER_DRAW_THRESHOLD = 16; + + public Dealer() { + super(new Name(DEFAULT_DEALER_NAME)); + } + + @Override + public boolean canReceiveCard() { + return getScore() <= DEALER_DRAW_THRESHOLD; + } +} 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..a7d6d08545 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/GameResult.java @@ -0,0 +1,11 @@ +package blackjack.domain.gamer; + +public enum GameResult { + + WIN, + LOSE; + + public static boolean isLose(GameResult gameResult) { + return LOSE == gameResult; + } +} 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..41280fb21e --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Hand.java @@ -0,0 +1,68 @@ +package blackjack.domain.gamer; + +import blackjack.domain.card.Card; +import java.util.List; + +public class Hand { + + private static final int BLACKJACK_MAX_SCORE = 21; + private static final int ACE_VALUE_MODIFIER = 10; + + private final List cards; + + public Hand(List cards) { + this.cards = cards; + } + + public void add(Card card) { + cards.add(card); + } + + public int calculateScore() { + int sum = cards.stream() + .mapToInt(Card::getNumberValue) + .sum(); + + if (hasAce()) { + return adjustSumWithAce(sum); + } + return sum; + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + /** + * ACE가 포함된 경우, 21 이하이면서 가장 가능한 큰 값으로 계산한다. + */ + 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; + } + + /** + * Ace는 기본적으로 11로 계산되나, 합계가 21을 초과할 경우 1로 계산한다. + */ + private int adjust(int sum) { + if (sum > BLACKJACK_MAX_SCORE) { + sum -= ACE_VALUE_MODIFIER; + } + return sum; + } + + public Card getFirstCard() { + return cards.get(0); + } + + public List getCards() { + return cards; + } +} 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..4c36a73f22 --- /dev/null +++ b/src/main/java/blackjack/domain/gamer/Player.java @@ -0,0 +1,27 @@ +package blackjack.domain.gamer; + +public class Player extends BlackjackGamer { + + private static final int BLACKJACK_MAX_SCORE = 21; + + public Player(Name name) { + super(name); + } + + @Override + public boolean canReceiveCard() { + return getScore() <= BLACKJACK_MAX_SCORE; + } + + public GameResult isWin(int dealerScore) { + int playerScore = getScore(); + + if (playerScore > BLACKJACK_MAX_SCORE) { + return GameResult.LOSE; + } + if (dealerScore > BLACKJACK_MAX_SCORE || playerScore > dealerScore) { + return GameResult.WIN; + } + return GameResult.LOSE; + } +} 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..646b54606b --- /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(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) { + players.forEach(player -> player.initCard(deck)); + } + + public Map collectPlayerGameResults(int dealerScore) { + Map playerGameResults = new LinkedHashMap<>(); + players.forEach(player -> playerGameResults.put(player.getName(), player.isWin(dealerScore))); + + 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 0000000000..274c3a9b63 --- /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 0000000000..37ef322c18 --- /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 first = dealer.getFirstCard(); + + return new DealerInitialHandDto(dealerName, CardDto.fromCard(first)); + } +} diff --git a/src/main/java/blackjack/dto/DealerResultDto.java b/src/main/java/blackjack/dto/DealerResultDto.java new file mode 100644 index 0000000000..bd1a2fff80 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerResultDto.java @@ -0,0 +1,17 @@ +package blackjack.dto; + +import blackjack.domain.gamer.GameResult; +import java.util.List; + +public record DealerResultDto(int winCount, int loseCount) { + + public static DealerResultDto fromPlayerResults(List playerResults) { + int dealerWinCount = (int) playerResults.stream() + .filter(GameResult::isLose) + .count(); + + int dealerLoseCount = playerResults.size() - dealerWinCount; + + return new DealerResultDto(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..52da8a1956 --- /dev/null +++ b/src/main/java/blackjack/dto/GamerHandDto.java @@ -0,0 +1,21 @@ +package blackjack.dto; + +import blackjack.domain.gamer.BlackjackGamer; +import blackjack.domain.gamer.Hand; +import java.util.List; + +public record GamerHandDto(String name, List gamerHand) { + + public static GamerHandDto fromBlackjackGamer(BlackjackGamer blackjackGamer) { + String playerName = blackjackGamer.getName().value(); + List gamerHand = convertHandToCardDto(blackjackGamer.getHand()); + + return new GamerHandDto(playerName, gamerHand); + } + + public static List convertHandToCardDto(Hand hand) { + return hand.getCards().stream() + .map(CardDto::fromCard) + .toList(); + } +} diff --git a/src/main/java/blackjack/dto/GamersHandDto.java b/src/main/java/blackjack/dto/GamersHandDto.java new file mode 100644 index 0000000000..e63516b98e --- /dev/null +++ b/src/main/java/blackjack/dto/GamersHandDto.java @@ -0,0 +1,15 @@ +package blackjack.dto; + +import blackjack.domain.gamer.Players; +import java.util.List; + +public record GamersHandDto(List gamersHandDto) { + + public static GamersHandDto fromPlayers(Players players) { + List gamerHandDtos = players.getPlayers().stream() + .map(GamerHandDto::fromBlackjackGamer) + .toList(); + + return new GamersHandDto(gamerHandDtos); + } +} diff --git a/src/main/java/blackjack/dto/PlayerGameResultsDto.java b/src/main/java/blackjack/dto/PlayerGameResultsDto.java new file mode 100644 index 0000000000..bacf83827e --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerGameResultsDto.java @@ -0,0 +1,19 @@ +package blackjack.dto; + +import blackjack.domain.gamer.GameResult; +import blackjack.domain.gamer.Name; +import java.util.LinkedHashMap; +import java.util.Map; + +public record PlayerGameResultsDto(Map resultMap) { + + public static PlayerGameResultsDto fromPlayerGameResults(Map playerGameResults) { + Map gameResult = new LinkedHashMap<>(); + + for (Name playerName : playerGameResults.keySet()) { + gameResult.put(playerName.value(), playerGameResults.get(playerName)); + } + + return new PlayerGameResultsDto(gameResult); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..6adb3dcefc --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,25 @@ +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 Command receiveCommand(String name) { + System.out.println(name + "는(은) 한장의 카드를 더 받겠습니까?(예는 y, 아니오: n)"); + 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 0000000000..393e4d9edb --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,110 @@ +package blackjack.view; + +import blackjack.dto.CardDto; +import blackjack.dto.DealerInitialHandDto; +import blackjack.dto.DealerResultDto; +import blackjack.dto.GamerHandDto; +import blackjack.dto.GamersHandDto; +import blackjack.dto.PlayerGameResultsDto; +import blackjack.view.object.CardNumberOutput; +import blackjack.view.object.CardShapeOutput; +import blackjack.view.object.GameResultOutput; +import java.util.List; + +public class OutputView { + + private static final String DEALER_DEFAULT_NAME = "딜러"; + + public void printInitialHands(DealerInitialHandDto dealerInitialHandDto, GamersHandDto playersInitialHandDto) { + System.out.printf("\n%s와 %s에게 2장을 나누었습니다.\n", dealerInitialHandDto.name(), + joinPlayerNames(playersInitialHandDto)); + + System.out.printf("%s 카드: %s\n", dealerInitialHandDto.name(), formatCardName(dealerInitialHandDto.firstCard())); + printAllPlayerHands(playersInitialHandDto); + } + + private String joinPlayerNames(GamersHandDto playersInitialHandDto) { + return String.join(", ", getPlayerNames(playersInitialHandDto)); + } + + private List getPlayerNames(GamersHandDto playersInitialHandDto) { + return playersInitialHandDto.gamersHandDto().stream().map(GamerHandDto::name).toList(); + } + + private String formatCardName(CardDto cardDto) { + return CardNumberOutput.convertNumberToOutput(cardDto.cardNumber()) + CardShapeOutput.convertShapeToOutput( + cardDto.cardShape()); + } + + private void printAllPlayerHands(GamersHandDto playersInitialHandDto) { + playersInitialHandDto.gamersHandDto().forEach(this::printPlayerHand); + System.out.println(); + } + + public void printPlayerHand(GamerHandDto playerHandDto) { + System.out.println(buildPlayerHand(playerHandDto)); + } + + private StringBuilder buildPlayerHand(GamerHandDto gamerHandDto) { + StringBuilder stringBuilder = new StringBuilder(); + return stringBuilder + .append(gamerHandDto.name()) + .append(" 카드: ") + .append(joinCardNames(gamerHandDto)); + } + + private String joinCardNames(GamerHandDto gamerHandDto) { + return String.join(", ", getCardNames(gamerHandDto)); + } + + private List getCardNames(GamerHandDto gamerHandDto) { + return gamerHandDto.gamerHand().stream().map(this::formatCardName).toList(); + } + + public void printDealerMessage(String dealerName) { + System.out.printf("\n%s는 16이하라 한장의 카드를 더 받았습니다.\n", dealerName); + } + + public void printScore(GamerHandDto gamerHandDto, int score) { + StringBuilder builder = buildPlayerHand(gamerHandDto) + .append(" - ") + .append("결과: ") + .append(score); + System.out.println(builder); + } + + public void printResult(DealerResultDto dealerResultDto, PlayerGameResultsDto playerGameResultsDto) { + System.out.println("\n## 최종 승패"); + printDealerResult(dealerResultDto); + printPlayerResults(playerGameResultsDto); + } + + private void printDealerResult(DealerResultDto dealerResultDto) { + int winCount = dealerResultDto.winCount(); + int loseCount = dealerResultDto.loseCount(); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder + .append(DEALER_DEFAULT_NAME) + .append(": "); + + if (winCount > 0) { + stringBuilder.append(winCount) + .append("승 "); + } + if (loseCount > 0) { + stringBuilder.append(loseCount) + .append("패"); + } + System.out.println(stringBuilder); + } + + private void printPlayerResults(PlayerGameResultsDto playerGameResultsDto) { + playerGameResultsDto.resultMap().forEach((name, result) -> + System.out.println(name + ": " + GameResultOutput.convertGameResultToOutput(result)) + ); + } + + 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 0000000000..03d4fa840f --- /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 0000000000..38b0374ce3 --- /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 0000000000..670cdbccf7 --- /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/main/java/blackjack/view/object/GameResultOutput.java b/src/main/java/blackjack/view/object/GameResultOutput.java new file mode 100644 index 0000000000..c41a70e3b8 --- /dev/null +++ b/src/main/java/blackjack/view/object/GameResultOutput.java @@ -0,0 +1,26 @@ +package blackjack.view.object; + +import blackjack.domain.gamer.GameResult; +import java.util.Arrays; + +public enum GameResultOutput { + + WIN_OUTPUT(GameResult.WIN, "승"), + LOSE_OUTPUT(GameResult.LOSE, "패"); + + private final GameResult gameResult; + private final String output; + + GameResultOutput(GameResult gameResult, String output) { + this.gameResult = gameResult; + this.output = output; + } + + public static String convertGameResultToOutput(GameResult gameResult) { + return Arrays.stream(values()) + .filter(gameResultOutput -> gameResultOutput.gameResult == gameResult) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("일치하는 게임 결과가 존재하지 않아 문자열을 반환할 수 없습니다.")) + .output; + } +} 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..3cb7939124 --- /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 0000000000..358bd4d9f0 --- /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/gamer/DealerTest.java b/src/test/java/blackjack/domain/gamer/DealerTest.java new file mode 100644 index 0000000000..9c98fa3db0 --- /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 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..5f1fee38ed --- /dev/null +++ b/src/test/java/blackjack/domain/gamer/PlayerTest.java @@ -0,0 +1,104 @@ +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 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 { + + Player player; + + @BeforeEach + void setUP() { + player = new Player(new Name("hogi")); + } + + @Nested + @DisplayName("카드를 받는 조건 테스트") + class CanReceiveCardTest { + + @Test + @DisplayName("카드의 총합이 21 이하이면 카드를 받을 수 있다.") + void receiveCardTest() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.SIX)); + 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 bustTest() { + 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.isWin(20)).isEqualTo(GameResult.WIN); + } + + @Test + @DisplayName("플레이어 점수가 21 이하이고, 딜러의 점수가 21을 초과하면 승리한다.") + void bustTest1() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.ACE)); + + assertThat(player.isWin(22)).isEqualTo(GameResult.WIN); + } + } + + @Nested + @DisplayName("플레이어가 패배하는 경우의 수 테스트") + class LoseTest { + + @Test + @DisplayName("점수가 21을 초과하면 딜러의 점수와 무관하게 패배한다") + void bustTest2() { + 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.isWin(21)).isEqualTo(GameResult.LOSE); + assertThat(player.isWin(22)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하일 때, 딜러의 점수보다 낮으면 패배한다.") + void bustTest3() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.SIX)); + + assertThat(player.isWin(17)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 모두 21 이하이고 동점인 경우 플레이어가 패배한다.") + void bustTest4() { + player.addCard(new Card(CardShape.CLOVER, CardNumber.KING)); + player.addCard(new Card(CardShape.HEART, CardNumber.SEVEN)); + + assertThat(player.isWin(17)).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("이름은 중복될 수 없습니다."); + } +}