diff --git a/.gitmessage.txt b/.gitmessage.txt new file mode 100644 index 00000000000..d389cb1c637 --- /dev/null +++ b/.gitmessage.txt @@ -0,0 +1,4 @@ + + + +Co-authored-by: haiseong \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..79f53e860df --- /dev/null +++ b/docs/README.md @@ -0,0 +1,32 @@ +## 기능 요구사항 정리 + +### 카드 +- [x] 카드는 4가지(스페이드, 클로버, 하트, 다이아몬드) 문양중 하나를 가진다. +- [x] 카드는 2~9의 숫자 또는 'A', 'J', 'Q', 'K'의 문자를 가진다. + +### 덱 +- [x] 자신이 가지고 있는 카드 중 한 장을 플레이어에게 제공할 수 있다. + +### 플레이어가 받은 카드 +- [x] 플레이어가 받은 한 장의 카드를 추가할 수 있다. +- [x] 받은 카드의 총 점수를 계산할 수 있다. + - 숫자 카드는 해당 숫자만큼의 점수로 계산된다. + - J, Q, K 카드는 모두 10으로 계산된다. +- [x] A 카드는 1 또는 11 중 하나를 선택하여 계산할 수 있다. +- [x] 현재 패의 버스트 여부를 판단할 수 있다. + +### 플레이어 +- [x] 덱으로 부터 카드 한장을 받아올 수 있다. +- [x] 자신의 점수를 계산할 수 있다. +- [x] 자신의 버스트 여부를 판단할 수 있다. +- [x] 중복된 이름이 있으면 예외가 발생한다. + +### 딜러 +- [x] 덱으로 부터 카드 한장을 받아올 수 있다. +- [x] 자신의 점수를 계산할 수 있다. +- [x] 자신의 버스트 여부를 판단할 수 있다. +- [x] 자신의 현재 점수가 17점 이상이 될 때까지 추가로 카드를 받는다. + +### 게임 결과 계산 +- [x] 플레이어들의 승리/패배/무승부 여부를 계산할 수 있다. +- [x] 딜러의 전적을 계산할 수 있다. diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..76839b84938 --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,14 @@ +package blackjack; + +import blackjack.controller.BlackJackController; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class Application { + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + BlackJackController blackJackController = new BlackJackController(inputView, outputView); + blackJackController.start(); + } +} diff --git a/src/main/java/blackjack/controller/BlackJackController.java b/src/main/java/blackjack/controller/BlackJackController.java new file mode 100644 index 00000000000..90e8f14eb61 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackJackController.java @@ -0,0 +1,111 @@ +package blackjack.controller; + +import blackjack.domain.Dealer; +import blackjack.domain.Deck; +import blackjack.domain.GameResultBoard; +import blackjack.domain.Player; +import blackjack.domain.Players; +import blackjack.domain.card.Card; +import blackjack.domain.dto.PlayerDto; +import blackjack.domain.dto.PlayerResultDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.List; + +public class BlackJackController { + public static final int INITIAL_CARDS_COUNT = 2; + private final InputView inputView; + private final OutputView outputView; + + public BlackJackController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void start() { + Dealer dealer = new Dealer(); + Players players = Players.from(inputView.inputPlayerNames()); + Deck deck = Deck.createShuffledDeck(); + + playGame(dealer, players, deck); + printGameResult(dealer, players); + } + + private void playGame(Dealer dealer, Players players, Deck deck) { + doInitialDraw(dealer, players, deck); + + players.getPlayers().forEach( + player -> doRound(player, deck) + ); + doDealerRound(dealer, deck); + } + + private void doInitialDraw(Dealer dealer, Players players, Deck deck) { + players.getPlayers().forEach( + player -> drawCard(player, deck, INITIAL_CARDS_COUNT) + ); + drawCard(dealer.getPlayer(), deck, INITIAL_CARDS_COUNT); + + outputView.printInitialMessage(players.getPlayerNames()); + printInitialCards(dealer, players); + } + + private void drawCard(Player player, Deck deck, int amount) { + for (int i = 0; i < amount; i++) { + player.draw(deck); + } + } + + private void printInitialCards(Dealer dealer, Players players) { + List dealerCards = dealer.getCards(); + outputView.printDealerInitialCard(dealerCards.get(0)); + + List playerDtos = players.getPlayers().stream() + .map(PlayerDto::from) + .toList(); + outputView.printPlayerInitialCards(playerDtos); + } + + private void doRound(Player player, Deck deck) { + while (!player.isBusted() && hasAdditionalCardRequest(player)) { + player.draw(deck); + outputView.printPlayerCard(PlayerDto.from(player)); + } + } + + private boolean hasAdditionalCardRequest(Player player) { + return inputView.inputDrawChoice(player.getName()); + } + + private void doDealerRound(Dealer dealer, Deck deck) { + dealer.drawUntilExceedMinimum(deck); + printExtraDealerDraw(dealer); + } + + private void printExtraDealerDraw(Dealer dealer) { + int dealerCardsCount = dealer.getCardsCount(); + int extraDrawCount = dealerCardsCount - INITIAL_CARDS_COUNT; + if (extraDrawCount > 0) { + outputView.printExtraDealerDraw(extraDrawCount); + } + } + + private void printGameResult(Dealer dealer, Players players) { + printCardStatus(dealer, players); + GameResultBoard gameResultBoard = new GameResultBoard(dealer, players.getPlayers()); + + outputView.printDealerResult(gameResultBoard.getDealerResult()); + for (Player player : players.getPlayers()) { + outputView.printPlayerResult(player.getName(), gameResultBoard.getGameResult(player)); + } + } + + private void printCardStatus(Dealer dealer, Players players) { + PlayerResultDto dealerResult = PlayerResultDto.from(dealer.getPlayer()); + + List playerResultDtos = players.getPlayers().stream() + .map(PlayerResultDto::from) + .toList(); + outputView.printCardStatus(dealerResult, playerResultDtos); + } +} diff --git a/src/main/java/blackjack/domain/Dealer.java b/src/main/java/blackjack/domain/Dealer.java new file mode 100644 index 00000000000..6741aa3b323 --- /dev/null +++ b/src/main/java/blackjack/domain/Dealer.java @@ -0,0 +1,40 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import java.util.List; + +public class Dealer { + private static final String DEALER_NAME = "딜러"; + + private final Player player; + + public Dealer() { + this.player = Player.fromName(DEALER_NAME); + } + + public void draw(Deck deck) { + player.draw(deck); + } + + public void drawUntilExceedMinimum(Deck deck) { + while (getScore().isLessThanDealerMinimumScore()) { + draw(deck); + } + } + + public List getCards() { + return player.getCards(); + } + + public Score getScore() { + return player.getScore(); + } + + public Player getPlayer() { + return player; + } + + public int getCardsCount() { + return player.getTotalCardsCount(); + } +} diff --git a/src/main/java/blackjack/domain/Deck.java b/src/main/java/blackjack/domain/Deck.java new file mode 100644 index 00000000000..bfdddd1b05f --- /dev/null +++ b/src/main/java/blackjack/domain/Deck.java @@ -0,0 +1,53 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class Deck { + private static final int SHUFFLED_DECK_SIZE = 52; + + private final Queue cards; + + public Deck(List cards) { + validateUniqueCard(cards); + this.cards = new LinkedList<>(cards); + } + + private void validateUniqueCard(List cards) { + int distinctCount = (int) cards.stream() + .distinct() + .count(); + + if (distinctCount != cards.size()) { + throw new IllegalArgumentException("중복되는 카드가 있습니다."); + } + } + + public static Deck createShuffledDeck() { + List cards = new ArrayList<>(SHUFFLED_DECK_SIZE); + + for (Shape shape : Shape.values()) { + cards.addAll(createAllCardsOf(shape)); + } + Collections.shuffle(cards); + + return new Deck(cards); + } + + private static List createAllCardsOf(Shape shape) { + return Arrays.stream(Value.values()) + .map(value -> new Card(shape, value)) + .toList(); + } + + public Card draw() { + return cards.poll(); + } +} diff --git a/src/main/java/blackjack/domain/GameResult.java b/src/main/java/blackjack/domain/GameResult.java new file mode 100644 index 00000000000..9d0b0006160 --- /dev/null +++ b/src/main/java/blackjack/domain/GameResult.java @@ -0,0 +1,22 @@ +package blackjack.domain; + +public enum GameResult { + WIN, LOSE, DRAW; + + public static GameResult calculatePlayerResult(Score playerScore, Score dealerScore) { + if (playerScore.isBusted()) { + return LOSE; + } + if (dealerScore.isBusted()) { + return WIN; + } + + if (playerScore.isGreaterThan(dealerScore)) { + return WIN; + } + if (playerScore.isLessThan(dealerScore)) { + return LOSE; + } + return DRAW; + } +} diff --git a/src/main/java/blackjack/domain/GameResultBoard.java b/src/main/java/blackjack/domain/GameResultBoard.java new file mode 100644 index 00000000000..615fabfb3f6 --- /dev/null +++ b/src/main/java/blackjack/domain/GameResultBoard.java @@ -0,0 +1,49 @@ +package blackjack.domain; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GameResultBoard { + private final Map resultBoard = new HashMap<>(); + + public GameResultBoard(Dealer dealer, List players) { + Score dealerScore = dealer.getScore(); + for (Player player : players) { + String playerName = player.getName(); + Score playerScore = player.getScore(); + GameResult gameResult = GameResult.calculatePlayerResult(playerScore, dealerScore); + resultBoard.put(playerName, gameResult); + } + } + + public GameResult getGameResult(Player player) { + return resultBoard.get(player.getName()); + } + + public Map getDealerResult() { + return Map.of( + GameResult.WIN, getDealerWinCount(), + GameResult.DRAW, getDealerDrawCount(), + GameResult.LOSE, getDealerLoseCount() + ); + } + + private int getDealerWinCount() { + return (int) resultBoard.values().stream() + .filter(playerResult -> playerResult.equals(GameResult.LOSE)) + .count(); + } + + private int getDealerLoseCount() { + return (int) resultBoard.values().stream() + .filter(playerResult -> playerResult.equals(GameResult.WIN)) + .count(); + } + + private int getDealerDrawCount() { + return (int) resultBoard.values().stream() + .filter(playerResult -> playerResult.equals(GameResult.DRAW)) + .count(); + } +} diff --git a/src/main/java/blackjack/domain/Player.java b/src/main/java/blackjack/domain/Player.java new file mode 100644 index 00000000000..8e5620071ae --- /dev/null +++ b/src/main/java/blackjack/domain/Player.java @@ -0,0 +1,47 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import java.util.List; + +public class Player { + private final PlayerName playerName; + private final PlayerCards playerCards; + + public Player(PlayerName playerName) { + this.playerName = playerName; + this.playerCards = PlayerCards.createEmptyCards(); + } + + public static Player fromName(String name) { + return new Player(new PlayerName(name)); + } + + public void draw(Deck deck) { + Card card = deck.draw(); + playerCards.append(card); + } + + public boolean isBusted() { + return playerCards.isBusted(); + } + + public List getCards() { + return playerCards.getCards(); + } + + public Score getScore() { + return playerCards.calculateScore(); + } + + public int getScoreValue() { + return getScore().value(); + } + + public String getName() { + return playerName.name(); + } + + public int getTotalCardsCount() { + return playerCards.size(); + } +} diff --git a/src/main/java/blackjack/domain/PlayerCards.java b/src/main/java/blackjack/domain/PlayerCards.java new file mode 100644 index 00000000000..62ea8536f79 --- /dev/null +++ b/src/main/java/blackjack/domain/PlayerCards.java @@ -0,0 +1,52 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import java.util.ArrayList; +import java.util.List; + +public class PlayerCards { + private final List cards; + + public PlayerCards(List cards) { + this.cards = cards; + } + + public static PlayerCards createEmptyCards() { + return new PlayerCards(new ArrayList<>()); + } + + public void append(Card card) { + cards.add(card); + } + + public Score calculateScore() { + int scoreValue = cards.stream() + .mapToInt(Card::getScore) + .sum(); + + Score score = new Score(scoreValue); + int currentAceAmount = getAceCount(); + + if (currentAceAmount > 0 && score.isBusted()) { + return score.convertToSmallAce(currentAceAmount); + } + return score; + } + + public boolean isBusted() { + Score score = calculateScore(); + return score.isBusted(); + } + + private int getAceCount() { + return (int) cards.stream().filter(Card::isAce).count(); + } + + public List getCards() { + return cards; + } + + public int size() { + return cards.size(); + } +} diff --git a/src/main/java/blackjack/domain/PlayerName.java b/src/main/java/blackjack/domain/PlayerName.java new file mode 100644 index 00000000000..de35d9f90bf --- /dev/null +++ b/src/main/java/blackjack/domain/PlayerName.java @@ -0,0 +1,9 @@ +package blackjack.domain; + +public record PlayerName(String name) { + public PlayerName { + if (name.isBlank()) { + throw new IllegalArgumentException("이름이 비어있습니다."); + } + } +} diff --git a/src/main/java/blackjack/domain/Players.java b/src/main/java/blackjack/domain/Players.java new file mode 100644 index 00000000000..440fc8383be --- /dev/null +++ b/src/main/java/blackjack/domain/Players.java @@ -0,0 +1,45 @@ +package blackjack.domain; + +import java.util.List; + +public class Players { + private final List playerGroup; + + public Players(List playerGroup) { + validate(playerGroup); + this.playerGroup = playerGroup; + } + + private static void validate(List players) { + if (duplicatedNameExist(players)) { + throw new IllegalArgumentException("중복된 이름이 존재합니다."); + } + } + + private static boolean duplicatedNameExist(List players) { + int distinctCount = (int) players.stream() + .map(Player::getName) + .distinct() + .count(); + + return distinctCount != players.size(); + } + + public static Players from(List names) { + List players = names.stream() + .map(Player::fromName) + .toList(); + + return new Players(players); + } + + public List getPlayers() { + return playerGroup; + } + + public List getPlayerNames() { + return playerGroup.stream() + .map(Player::getName) + .toList(); + } +} diff --git a/src/main/java/blackjack/domain/Score.java b/src/main/java/blackjack/domain/Score.java new file mode 100644 index 00000000000..e50d519dbe0 --- /dev/null +++ b/src/main/java/blackjack/domain/Score.java @@ -0,0 +1,53 @@ +package blackjack.domain; + +public record Score(int value) { + private static final int MINIMUM_VALUE = 0; + private static final int BUST_THRESHOLD = 21; + private static final int DEALER_MINIMUM_SCORE = 17; + public static final int CONVERTED_ACE_DIFFERENCE = 10; + + public Score { + validateRange(value); + } + + private void validateRange(int value) { + if (value < MINIMUM_VALUE) { + throw new IllegalArgumentException("음수는 점수로 사용될 수 없습니다."); + } + } + + public boolean isBusted() { + return value > BUST_THRESHOLD; + } + + private boolean isBusted(int scoreValue) { + return scoreValue > BUST_THRESHOLD; + } + + public boolean isGreaterThan(Score relativeScore) { + return value > relativeScore.value; + } + + public boolean isSame(Score relativeScore) { + return value == relativeScore.value; + } + + public boolean isLessThan(Score relativeScore) { + return value < relativeScore.value; + } + + public boolean isLessThanDealerMinimumScore() { + return value < DEALER_MINIMUM_SCORE; + } + + public Score convertToSmallAce(int currentBigAceAmount) { + int currentValue = value; + int convertedAceAmount = 0; + while (isBusted(currentValue) && convertedAceAmount < currentBigAceAmount) { + currentValue -= CONVERTED_ACE_DIFFERENCE; + convertedAceAmount++; + } + + return new Score(currentValue); + } +} 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..0b7b2c05f4e --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,11 @@ +package blackjack.domain.card; + +public record Card(Shape shape, Value value) { + public int getScore() { + return value().getScore(); + } + + public boolean isAce() { + return value == Value.ACE; + } +} diff --git a/src/main/java/blackjack/domain/card/Shape.java b/src/main/java/blackjack/domain/card/Shape.java new file mode 100644 index 00000000000..ee270ba85a8 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Shape.java @@ -0,0 +1,5 @@ +package blackjack.domain.card; + +public enum Shape { + SPADE, HEART, DIAMOND, CLOVER +} diff --git a/src/main/java/blackjack/domain/card/Value.java b/src/main/java/blackjack/domain/card/Value.java new file mode 100644 index 00000000000..04117d7cef9 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Value.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +public enum Value { + 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); + + final int score; + + Value(int score) { + this.score = score; + } + + public int getScore() { + return score; + } +} diff --git a/src/main/java/blackjack/domain/dto/PlayerDto.java b/src/main/java/blackjack/domain/dto/PlayerDto.java new file mode 100644 index 00000000000..d24e84dd1d0 --- /dev/null +++ b/src/main/java/blackjack/domain/dto/PlayerDto.java @@ -0,0 +1,11 @@ +package blackjack.domain.dto; + +import blackjack.domain.Player; +import blackjack.domain.card.Card; +import java.util.List; + +public record PlayerDto(String name, List cards) { + public static PlayerDto from(Player player) { + return new PlayerDto(player.getName(), player.getCards()); + } +} diff --git a/src/main/java/blackjack/domain/dto/PlayerResultDto.java b/src/main/java/blackjack/domain/dto/PlayerResultDto.java new file mode 100644 index 00000000000..06b5474a8e5 --- /dev/null +++ b/src/main/java/blackjack/domain/dto/PlayerResultDto.java @@ -0,0 +1,19 @@ +package blackjack.domain.dto; + +import blackjack.domain.Player; +import blackjack.domain.card.Card; +import java.util.List; + +public record PlayerResultDto(PlayerDto playerDto, int score) { + public static PlayerResultDto from(Player player) { + return new PlayerResultDto(PlayerDto.from(player), player.getScoreValue()); + } + + public String getName() { + return playerDto.name(); + } + + public List getCards() { + return playerDto.cards(); + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..c4b5877307a --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,27 @@ +package blackjack.view; + + +import java.util.List; +import java.util.Scanner; + +public class InputView { + private final Scanner scanner = new Scanner(System.in); + + public List inputPlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String line = scanner.nextLine(); + return List.of(line.split(",")); + } + + public boolean inputDrawChoice(String playerName) { + System.out.println(playerName + "는 한장의 카드를 더 받겠습니까? (y/n)"); + String choice = scanner.nextLine(); + if ("y".equals(choice)) { + return true; + } + if ("n".equals(choice)) { + return false; + } + throw new IllegalArgumentException("y 또는 n만 선택할 수 있습니다."); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000000..60c522bc6b1 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,91 @@ +package blackjack.view; + +import blackjack.domain.GameResult; +import blackjack.domain.card.Card; +import blackjack.domain.dto.PlayerDto; +import blackjack.domain.dto.PlayerResultDto; +import blackjack.view.description.GameResultDescription; +import blackjack.view.description.ShapeDescription; +import blackjack.view.description.ValueDescription; +import java.util.List; +import java.util.Map; + +public class OutputView { + public void printPlayerInitialCards(List playerDtos) { + StringBuilder stringBuilder = new StringBuilder(); + + playerDtos.forEach(playerDto -> + stringBuilder.append(playerDto.name()) + .append("카드: ") + .append(generateCardsDescription(playerDto.cards())) + .append(System.lineSeparator()) + ); + + System.out.println(stringBuilder); + } + + public void printDealerInitialCard(Card dealerCard) { + System.out.println("딜러: " + generateCardDescription(dealerCard)); + } + + private String generateCardsDescription(List cards) { + List list = cards.stream() + .map(this::generateCardDescription) + .toList(); + return String.join(", ", list); + } + + private String generateCardDescription(Card card) { + String shapeDescription = ShapeDescription.getDescription(card.shape()); + String valueDescription = ValueDescription.getDescription(card.value()); + return shapeDescription + valueDescription; + } + + public void printInitialMessage(List playerNames) { + System.out.println("딜러와 " + String.join(", ", playerNames) + " 에게 2장을 나누었습니다."); + } + + public void printPlayerCard(PlayerDto playerDto) { + String name = playerDto.name(); + System.out.println(name + "카드: " + generateCardsDescription(playerDto.cards())); + } + + public void printExtraDealerDraw(int extraDrawCount) { + System.out.println("딜러는 16이하라 " + extraDrawCount + "장의 카드를 더 받았습니다."); + } + + public void printCardStatus(PlayerResultDto dealerResult, List playerResultDtos) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("딜러 카드: ") + .append(generateCardsDescription(dealerResult.getCards())) + .append(" - 결과: ") + .append(dealerResult.score()) + .append(System.lineSeparator()); + + playerResultDtos.forEach(playerResultDto -> + stringBuilder.append(playerResultDto.getName()) + .append("카드: ") + .append(generateCardsDescription(playerResultDto.getCards())) + .append(" - 결과: ") + .append(playerResultDto.score()) + .append(System.lineSeparator()) + ); + + System.out.println(stringBuilder); + } + + public void printDealerResult(Map dealerResult) { + String result = "## 최종 승패" + System.lineSeparator() + + "딜러: " + + dealerResult.get(GameResult.WIN) + GameResultDescription.WIN.getDescription() + + dealerResult.get(GameResult.DRAW) + GameResultDescription.DRAW.getDescription() + + dealerResult.get(GameResult.LOSE) + GameResultDescription.LOSE.getDescription(); + + System.out.println(result); + } + + public void printPlayerResult(String playerName, GameResult gameResult) { + System.out.println(playerName + ": " + GameResultDescription.getDescription(gameResult)); + } +} diff --git a/src/main/java/blackjack/view/description/GameResultDescription.java b/src/main/java/blackjack/view/description/GameResultDescription.java new file mode 100644 index 00000000000..629dfe293cf --- /dev/null +++ b/src/main/java/blackjack/view/description/GameResultDescription.java @@ -0,0 +1,30 @@ +package blackjack.view.description; + +import blackjack.domain.GameResult; +import java.util.Arrays; + +public enum GameResultDescription { + WIN(GameResult.WIN, "승"), + LOSE(GameResult.LOSE, "패"), + DRAW(GameResult.DRAW, "무"); + + private final GameResult gameResult; + private final String description; + + GameResultDescription(GameResult gameResult, String description) { + this.gameResult = gameResult; + this.description = description; + } + + public String getDescription() { + return description; + } + + public static String getDescription(GameResult result) { + return Arrays.stream(values()) + .filter(resultDescription -> resultDescription.gameResult == result) + .findFirst() + .map(GameResultDescription::getDescription) + .orElseThrow(() -> new IllegalArgumentException("해당하는 값을 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/blackjack/view/description/ShapeDescription.java b/src/main/java/blackjack/view/description/ShapeDescription.java new file mode 100644 index 00000000000..e9e825ef90d --- /dev/null +++ b/src/main/java/blackjack/view/description/ShapeDescription.java @@ -0,0 +1,31 @@ +package blackjack.view.description; + +import blackjack.domain.card.Shape; +import java.util.Arrays; + +public enum ShapeDescription { + SPADE(Shape.SPADE, "스페이드"), + HEART(Shape.HEART, "하트"), + DIAMOND(Shape.DIAMOND, "다이아몬드"), + CLOVER(Shape.CLOVER, "클로버"); + + private final Shape shape; + private final String description; + + ShapeDescription(Shape shape, String description) { + this.shape = shape; + this.description = description; + } + + public String getDescription() { + return description; + } + + public static String getDescription(Shape shape) { + return Arrays.stream(values()) + .filter(shapeDescription -> shapeDescription.shape == shape) + .findFirst() + .map(ShapeDescription::getDescription) + .orElseThrow(() -> new IllegalArgumentException("해당하는 값을 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/blackjack/view/description/ValueDescription.java b/src/main/java/blackjack/view/description/ValueDescription.java new file mode 100644 index 00000000000..09839cd3acf --- /dev/null +++ b/src/main/java/blackjack/view/description/ValueDescription.java @@ -0,0 +1,40 @@ +package blackjack.view.description; + +import blackjack.domain.card.Value; +import java.util.Arrays; + +public enum ValueDescription { + ACE(Value.ACE, "A"), + TWO(Value.TWO, "2"), + THREE(Value.THREE, "3"), + FOUR(Value.FOUR, "4"), + FIVE(Value.FIVE, "5"), + SIX(Value.SIX, "6"), + SEVEN(Value.SEVEN, "7"), + EIGHT(Value.EIGHT, "8"), + NINE(Value.NINE, "9"), + TEN(Value.TEN, "10"), + JACK(Value.JACK, "J"), + QUEEN(Value.QUEEN, "Q"), + KING(Value.KING, "K"); + + final Value value; + final String description; + + ValueDescription(Value value, String description) { + this.value = value; + this.description = description; + } + + public String getDescription() { + return description; + } + + public static String getDescription(Value value) { + return Arrays.stream(values()) + .filter(valueDescription -> valueDescription.value == value) + .findFirst() + .map(ValueDescription::getDescription) + .orElseThrow(() -> new IllegalArgumentException("해당하는 값을 찾을 수 없습니다.")); + } +} diff --git a/src/test/java/blackjack/domain/DealerTest.java b/src/test/java/blackjack/domain/DealerTest.java new file mode 100644 index 00000000000..f90640797be --- /dev/null +++ b/src/test/java/blackjack/domain/DealerTest.java @@ -0,0 +1,61 @@ +package blackjack.domain; + +import static blackjack.domain.card.Shape.DIAMOND; +import static blackjack.domain.card.Value.ACE; +import static blackjack.domain.card.Value.FOUR; +import static blackjack.domain.card.Value.KING; +import static blackjack.domain.card.Value.QUEEN; +import static blackjack.domain.card.Value.THREE; +import static blackjack.domain.card.Value.TWO; +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import java.util.List; +import java.util.stream.Stream; +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 DealerTest { + @Test + @DisplayName("딜러가 카드 한 장을 뽑는다.") + void drawCardTest() { + Deck deck = new Deck(List.of( + new Card(DIAMOND, KING), new Card(DIAMOND, TWO), new Card(DIAMOND, FOUR) + )); + Dealer dealer = new Dealer(); + + dealer.draw(deck); + Score score = dealer.getScore(); + Score expected = new Score(10); + + assertThat(score).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("cardsAndScore") + @DisplayName("자신의 현재 점수가 17점 이상이 될 때까지 추가로 카드를 받는다.") + void drawCardsUntilScoreBelow17Test(List cards, int expectedValue) { + Deck deck = new Deck(cards); + Dealer dealer = new Dealer(); + + dealer.drawUntilExceedMinimum(deck); + Score score = dealer.getScore(); + Score expected = new Score(expectedValue); + + assertThat(score).isEqualTo(expected); + } + + static Stream cardsAndScore() { + return Stream.of( + Arguments.arguments(List.of( + new Card(DIAMOND, KING), new Card(DIAMOND, TWO), new Card(DIAMOND, FOUR), + new Card(DIAMOND, ACE), new Card(DIAMOND, THREE)), 17), + Arguments.arguments(List.of( + new Card(DIAMOND, KING), new Card(DIAMOND, ACE), + new Card(DIAMOND, QUEEN)), 21) + ); + } +} diff --git a/src/test/java/blackjack/domain/DeckTest.java b/src/test/java/blackjack/domain/DeckTest.java new file mode 100644 index 00000000000..d17e26c7f93 --- /dev/null +++ b/src/test/java/blackjack/domain/DeckTest.java @@ -0,0 +1,42 @@ +package blackjack.domain; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Value; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DeckTest { + @Test + @DisplayName("자신이 가지고 있는 카드 중 한 장을 플레이어에게 제공할 수 있다.") + void draw() { + List cards = new ArrayList<>(List.of( + new Card(Shape.SPADE, Value.ACE), + new Card(Shape.CLOVER, Value.FOUR), + new Card(Shape.HEART, Value.KING) + )); + Deck deck = new Deck(cards); + + Card expected = new Card(Shape.SPADE, Value.ACE); + assertThat(deck.draw()).isEqualTo(expected); + } + + @Test + @DisplayName("중복되는 카드가 있다면 예외가 발생한다.") + void duplicatedCardsTest() { + List cards = List.of( + new Card(Shape.DIAMOND, Value.FOUR), + new Card(Shape.DIAMOND, Value.FOUR) + ); + + assertThatThrownBy(() -> new Deck(cards)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("중복되는 카드가 있습니다."); + } +} diff --git a/src/test/java/blackjack/domain/GameResultBoardTest.java b/src/test/java/blackjack/domain/GameResultBoardTest.java new file mode 100644 index 00000000000..4a1b0fb1c0a --- /dev/null +++ b/src/test/java/blackjack/domain/GameResultBoardTest.java @@ -0,0 +1,105 @@ +package blackjack.domain; + +import static blackjack.domain.card.Shape.DIAMOND; +import static blackjack.domain.card.Shape.SPADE; +import static blackjack.domain.card.Value.ACE; +import static blackjack.domain.card.Value.EIGHT; +import static blackjack.domain.card.Value.KING; +import static blackjack.domain.card.Value.NINE; +import static blackjack.domain.card.Value.QUEEN; +import static blackjack.domain.card.Value.TEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import blackjack.domain.card.Card; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +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 GameResultBoardTest { + @ParameterizedTest + @MethodSource("playerAndGameResult") + @DisplayName("딜러와 플레이어들을 전달받아 승리/패배/무승부 여부를 계산한다.") + void calculateGameResultTest(List playerCards, GameResult expected) { + List cards = new ArrayList<>(List.of(new Card(DIAMOND, KING), new Card(DIAMOND, NINE))); + cards.addAll(playerCards); + Deck deck = new Deck(cards); + + Dealer dealer = new Dealer(); + giveCardToPlayer(dealer.getPlayer(), deck, 2); + Player player = Player.fromName("testPlayer"); + giveCardToPlayer(player, deck, 2); + + GameResultBoard gameResultBoard = new GameResultBoard(dealer, List.of(player)); + + assertThat(gameResultBoard.getGameResult(player)).isEqualTo(expected); + } + + static Stream playerAndGameResult() { + return Stream.of( + Arguments.arguments( + List.of(new Card(DIAMOND, ACE), new Card(DIAMOND, QUEEN)), GameResult.WIN + ), + Arguments.arguments( + List.of(new Card(SPADE, ACE), new Card(SPADE, QUEEN)), GameResult.WIN + ), + Arguments.arguments( + List.of(new Card(SPADE, KING), new Card(SPADE, NINE)), GameResult.DRAW + ), + Arguments.arguments( + List.of(new Card(SPADE, TEN), new Card(SPADE, EIGHT)), GameResult.LOSE + ) + ); + } + + @Test + @DisplayName("딜러의 전적을 반환할 수 있다.") + void calculateDealerResultTest() { + List cards = generateCards(); + Deck deck = new Deck(cards); + + Dealer dealer = new Dealer(); + giveCardToPlayer(dealer.getPlayer(), deck, 2); + + List players = generatePlayers(); + players.forEach(player -> giveCardToPlayer(player, deck, 2)); + + GameResultBoard gameResultBoard = new GameResultBoard(dealer, players); + Map dealerResult = gameResultBoard.getDealerResult(); + + assertAll( + () -> assertThat(dealerResult).containsEntry(GameResult.WIN, 1), + () -> assertThat(dealerResult).containsEntry(GameResult.DRAW, 1), + () -> assertThat(dealerResult).containsEntry(GameResult.LOSE, 2) + ); + } + + private List generateCards() { + return List.of( + new Card(DIAMOND, KING), new Card(DIAMOND, NINE), // Dealer + new Card(DIAMOND, ACE), new Card(DIAMOND, QUEEN), // dealer lose + new Card(SPADE, ACE), new Card(SPADE, QUEEN), // dealer lose + new Card(SPADE, KING), new Card(SPADE, NINE), // draw + new Card(SPADE, TEN), new Card(SPADE, EIGHT) // dealer win + ); + } + + private List generatePlayers() { + List playerNames = List.of("loki", "pedro", "poke", "alpaca"); + return playerNames.stream() + .map(Player::fromName) + .toList(); + } + + private void giveCardToPlayer(Player player, Deck deck, int drawAmount) { + for (int i = 0; i < drawAmount; i++) { + player.draw(deck); + } + } +} diff --git a/src/test/java/blackjack/domain/GameResultTest.java b/src/test/java/blackjack/domain/GameResultTest.java new file mode 100644 index 00000000000..ab5f87069c8 --- /dev/null +++ b/src/test/java/blackjack/domain/GameResultTest.java @@ -0,0 +1,53 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class GameResultTest { + @ParameterizedTest + @DisplayName("플레이어가 버스트 되지 않고 점수가 딜러보다 높으면 플레이어가 승리한다.") + @CsvSource({ + "20, 19", + "15, 22" + }) + void playerWinTest(int playerScoreValue, int dealerScoreValue) { + Score playerScore = new Score(playerScoreValue); + Score dealerScore = new Score(dealerScoreValue); + + GameResult winResult = GameResult.calculatePlayerResult(playerScore, dealerScore); + GameResult expected = GameResult.WIN; + assertThat(winResult).isEqualTo(expected); + } + + @ParameterizedTest + @DisplayName("플레이어가 버스트되거나 플레이어의 점수가 딜러보다 낮으면 플레이어가 패배한다.") + @CsvSource({ + "22, 18", + "18, 20", + "22, 22" + }) + void playerLoseTest(int playerScoreValue, int dealerScoreValue) { + Score playerScore = new Score(playerScoreValue); + Score dealerScore = new Score(dealerScoreValue); + + GameResult loseResult = GameResult.calculatePlayerResult(playerScore, dealerScore); + GameResult expected = GameResult.LOSE; + assertThat(loseResult).isEqualTo(expected); + } + + @Test + @DisplayName("플레이어와 딜러의 점수가 같은 경우 무승부로 판단한다.") + void drawTest() { + int sameScore = 21; + Score playerScore = new Score(sameScore); + Score dealerScore = new Score(sameScore); + + GameResult drawResult = GameResult.calculatePlayerResult(playerScore, dealerScore); + GameResult expected = GameResult.DRAW; + assertThat(drawResult).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/blackjack/domain/PlayerCardsTest.java b/src/test/java/blackjack/domain/PlayerCardsTest.java new file mode 100644 index 00000000000..96467b3cadd --- /dev/null +++ b/src/test/java/blackjack/domain/PlayerCardsTest.java @@ -0,0 +1,79 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static blackjack.domain.card.Shape.*; +import static blackjack.domain.card.Value.*; + +import blackjack.domain.card.Card; +import java.util.List; +import java.util.stream.Stream; +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 PlayerCardsTest { + @Test + @DisplayName("한 장의 카드를 추가할 수 있다.") + void addCardTest() { + Card card = new Card(DIAMOND, ACE); + PlayerCards playerCards = PlayerCards.createEmptyCards(); + + playerCards.append(card); + List cards = playerCards.getCards(); + assertThat(cards).hasSize(1); + } + + @Test + @DisplayName("숫자 카드는 해당 숫자만큼의 점수로 계산된다.") + void calculateScoreTest() { + List cards = List.of( + new Card(DIAMOND, TWO), + new Card(DIAMOND, THREE), + new Card(DIAMOND, FOUR) + ); + PlayerCards playerCards = new PlayerCards(cards); + + Score score = playerCards.calculateScore(); + Score expected = new Score(9); + + assertThat(score).isEqualTo(expected); + } + + @Test + @DisplayName("J, Q, K 카드는 모두 10으로 계산된다.") + void calculateScoreWithAlphabetTest() { + List cards = List.of( + new Card(DIAMOND, JACK), + new Card(DIAMOND, QUEEN), + new Card(DIAMOND, KING) + ); + PlayerCards playerCards = new PlayerCards(cards); + + Score score = playerCards.calculateScore(); + Score expected = new Score(30); + + assertThat(score).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("cardsAndScore") + @DisplayName("ACE 는 1점 혹은 11점으로 계산된다.") + void calculateScoreWithAce(List cards, int expectedValue) { + PlayerCards playerCards = new PlayerCards(cards); + Score score = playerCards.calculateScore(); + + Score expected = new Score(expectedValue); + assertThat(score).isEqualTo(expected); + } + + static Stream cardsAndScore() { + return Stream.of( + Arguments.arguments(List.of(new Card(DIAMOND, JACK), new Card(DIAMOND, ACE)), 21), + Arguments.arguments(List.of(new Card(DIAMOND, NINE), new Card(DIAMOND, ACE), new Card(SPADE, ACE)), 21), + Arguments.arguments(List.of(new Card(DIAMOND, KING), new Card(DIAMOND, QUEEN), new Card(DIAMOND, ACE)), 21), + Arguments.arguments(List.of(new Card(DIAMOND, ACE), new Card(CLOVER, ACE), new Card(SPADE, ACE)), 13) + ); + } +} diff --git a/src/test/java/blackjack/domain/PlayerTest.java b/src/test/java/blackjack/domain/PlayerTest.java new file mode 100644 index 00000000000..dce615dd22a --- /dev/null +++ b/src/test/java/blackjack/domain/PlayerTest.java @@ -0,0 +1,85 @@ +package blackjack.domain; + +import static blackjack.domain.card.Shape.DIAMOND; +import static blackjack.domain.card.Value.ACE; +import static blackjack.domain.card.Value.FOUR; +import static blackjack.domain.card.Value.JACK; +import static blackjack.domain.card.Value.QUEEN; +import static blackjack.domain.card.Value.THREE; +import static blackjack.domain.card.Value.TWO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import java.util.List; +import java.util.stream.Stream; +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; +import org.junit.jupiter.params.provider.ValueSource; + +class PlayerTest { + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", "\n"}) + @DisplayName("공백 이름으로는 플레이어를 생성하면 예외가 발생한다.") + void throwsExceptionWhenNameIsBlankTest(String blankName) { + assertThatThrownBy(() -> Player.fromName(blankName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름이 비어있습니다."); + } + + @Test + @DisplayName("덱으로 부터 카드 한장을 받아올 수 있다.") + void drawCardTest() { + List cards = List.of(new Card(DIAMOND, TWO), new Card(DIAMOND, THREE), new Card(DIAMOND, FOUR)); + Deck deck = new Deck(cards); + + Player player = Player.fromName("pedro"); + player.draw(deck); + + List playerCards = player.getCards(); + assertThat(playerCards).hasSize(1); + } + + @Test + @DisplayName("자신의 점수를 계산할 수 있다.") + void calculateScoreTest() { + List cards = List.of(new Card(DIAMOND, TWO), new Card(DIAMOND, THREE), new Card(DIAMOND, FOUR)); + Deck deck = new Deck(cards); + + Player player = Player.fromName("pedro"); + for (int i = 0; i < cards.size(); i++) { + player.draw(deck); + } + + Score score = player.getScore(); + + Score expected = new Score(9); + assertThat(score).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("cardsAndBustStatus") + @DisplayName("자신의 버스트 여부를 판단할 수 있다.") + void checkBustTest(List cards, boolean expected) { + Deck deck = new Deck(cards); + + Player player = Player.fromName("pedro"); + for (int i = 0; i < cards.size(); i++) { + player.draw(deck); + } + + boolean isBusted = player.isBusted(); + + assertThat(isBusted).isEqualTo(expected); + } + + static Stream cardsAndBustStatus() { + return Stream.of( + Arguments.arguments(List.of(new Card(DIAMOND, JACK), new Card(DIAMOND, QUEEN), new Card(DIAMOND, ACE)), false), + Arguments.arguments(List.of(new Card(DIAMOND, JACK), new Card(DIAMOND, QUEEN), new Card(DIAMOND, TWO)), true) + ); + } +} diff --git a/src/test/java/blackjack/domain/PlayersTest.java b/src/test/java/blackjack/domain/PlayersTest.java new file mode 100644 index 00000000000..ab9b2f5048d --- /dev/null +++ b/src/test/java/blackjack/domain/PlayersTest.java @@ -0,0 +1,20 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + @Test + @DisplayName("중복된 이름이 있으면 예외가 발생한다.") + void duplicatedNamesTest() { + List names = List.of("loki", "pedro", "loki"); + + assertThatThrownBy(() -> Players.from(names)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("중복된 이름이 존재합니다."); + } +} diff --git a/src/test/java/blackjack/domain/ScoreTest.java b/src/test/java/blackjack/domain/ScoreTest.java new file mode 100644 index 00000000000..9c695593838 --- /dev/null +++ b/src/test/java/blackjack/domain/ScoreTest.java @@ -0,0 +1,74 @@ +package blackjack.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ScoreTest { + @Test + @DisplayName("음수는 점수로 사용될 수 없다.") + void negativeScoreTest() { + int negativeValue = -1; + assertThatThrownBy(() -> new Score(negativeValue)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("음수는 점수로 사용될 수 없습니다."); + } + + @ParameterizedTest + @CsvSource({"21, false", "22, true"}) + @DisplayName("점수가 21점을 초과하면 버스트") + void burstIfExceed21Test(int value, boolean expected) { + Score score = new Score(value); + + assertThat(score.isBusted()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "21, 20, true", + "20, 20, false", + "19, 20, false", + }) + @DisplayName("현재 점수가 다른 점수보다 더 높은지 확인할 수 있다.") + void isGreaterThenTest(int currentValue, int relativeValue, boolean expected) { + Score currentScore = new Score(currentValue); + Score relativeScore = new Score(relativeValue); + + boolean actual = currentScore.isGreaterThan(relativeScore); + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "21, 20, false", + "20, 20, true", + "19, 20, false", + }) + @DisplayName("현재 점수가 다른 점수보다 더 높은지 확인할 수 있다.") + void isSameTest(int currentValue, int relativeValue, boolean expected) { + Score currentScore = new Score(currentValue); + Score relativeScore = new Score(relativeValue); + + boolean actual = currentScore.isSame(relativeScore); + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "21, 20, false", + "20, 20, false", + "19, 20, true", + }) + @DisplayName("현재 점수가 다른 점수보다 더 높은지 확인할 수 있다.") + void isLessThanTest(int currentValue, int relativeValue, boolean expected) { + Score currentScore = new Score(currentValue); + Score relativeScore = new Score(relativeValue); + + boolean actual = currentScore.isLessThan(relativeScore); + assertThat(actual).isEqualTo(expected); + } +}