diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..0fd5c784455 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,57 @@ +## 블랙잭 게임 기능 목록 +### UI +- 입력 + - [x] 참여할 사람의 이름 입력 + - [x] 카드 추가 지급 의사 입력 + +- 출력 + - [x] 카드 지급 완료 문구 출력 + - [x] 개인 카드 목록 출력 + - [x] 딜러 카드 추가 지급 여부 출력 + - [x] 최종 카드 상태 출력 + - [x] 최종 승패 출력 + +### 기능 +- BlackJackGame : 게임 관리 시스템 + - [x] 최종 승패를 계산한다 + +- Player : 게임 참여자 + - [x] 초기 카드 2장을 받는다 + - [x] 추가 카드를 뽑는다 + - Challenger : 일반 참여자 (Player를 상속받는다) + - [x] 이름을 입력받는다 + - [x] 카드의 합이 21 초과인지 확인한다 + - Dealer : 딜러 (Player를 상속받는다) + - [x] 카드의 합이 16 초과인지 확인한다 + +- Players : Player 목록을 가지고 있는 일급 컬렉션 + +- Card : 카드 + - [x] 카드 숫자를 보고 점수를 반환한다 (ex. Ace -> 1 or 11 // King, Queen, Jack -> 10) + +- CardDeck : 덱에 있는 카드. Card 목록을 가지고 있는 일급 컬렉션 + - [x] 52장의 카드를 생성한다 + - [x] 카드 순서를 무작위로 섞는다 + - [x] 맨 위의 카드를 게임 참여자에게 전달한다 + +- HolingCards : 참여자가 소유하고 있는 카드. Card 목록을 가지고 있는 일급 컬렉션 + - [x] 뽑은 카드를 저장한다 + - [x] 가진 카드로 가능한 합 목록을 반환한다 + - [x] 가진 카드들 중에 21보다 작은 가장 큰 값을 반환한다 + +- Result : 최종 결과를 계산하는 객체 + - [x] 도전자들의 최종 승패를 계산한다. + - [x] 딜러의 최종 승패를 계산한다. + +### 예외 사항 +- [x] 플레이어 이름 중복 시 예외가 발생한다 +- [x] 플레이어 이름이 `딜러`인 경우 예외가 발생한다 + +- [x] 카드를 더 받겠느냐는 질문에 `y`, `n`을 입력하지 않으면 예외가 발생한다 +- [x] 더 이상 뽑을 카드가 없으면 카드 덱이 비어있다는 예외가 발생한다 + +--------- +## 고민 +- dto에도 정적 팩토리 메소드를 써야할까? +- playerStatusDto(필드2개) & playerResultDto(필드3개) -> 필드2개 겹치는데 두 dto를 따로 만들어야하나? 아니면 필드를 안쓰더라도 하나로 통합?(중복방현) +- else if 사용? diff --git a/src/main/java/blackjack/Application.java b/src/main/java/blackjack/Application.java new file mode 100644 index 00000000000..3f73ad14652 --- /dev/null +++ b/src/main/java/blackjack/Application.java @@ -0,0 +1,10 @@ +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/common/exception/CustomException.java b/src/main/java/blackjack/common/exception/CustomException.java new file mode 100644 index 00000000000..72145fa46b2 --- /dev/null +++ b/src/main/java/blackjack/common/exception/CustomException.java @@ -0,0 +1,10 @@ +package blackjack.common.exception; + +public class CustomException extends RuntimeException { + + private static final String ERROR_PREFIX = "[ERROR] "; + + public CustomException(String message) { + super(ERROR_PREFIX + message); + } +} diff --git a/src/main/java/blackjack/controller/BlackJackController.java b/src/main/java/blackjack/controller/BlackJackController.java new file mode 100644 index 00000000000..45fb8a3e955 --- /dev/null +++ b/src/main/java/blackjack/controller/BlackJackController.java @@ -0,0 +1,164 @@ +package blackjack.controller; + +import blackjack.common.exception.CustomException; +import blackjack.domain.BlackJackGame; +import blackjack.domain.card.Card; +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.result.ResultMap; +import blackjack.domain.result.ResultType; +import blackjack.dto.ChallengerResultDto; +import blackjack.dto.DealerResultDto; +import blackjack.dto.PlayerStatusDto; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BlackJackController { + + private BlackJackGame blackJackGame; + + public void run() { + init(); + start(); + takeTurn(); + showResult(); + } + + private void init() { + try { + List playerNames = InputView.inputPlayerNames(); + blackJackGame = BlackJackGame.from(playerNames); + } catch (CustomException e) { + OutputView.printErrorMessage(e); + init(); + } + } + + private void start() { + blackJackGame.handOutStartCards(); + showStartStatus(); + } + + private void showStartStatus() { + PlayerStatusDto dealerStatus = makeDealerStatus(); + List challengersStatus = makeChallengersStatus(); + OutputView.printStartStatus(dealerStatus, challengersStatus); + } + + private PlayerStatusDto makeDealerStatus() { + Player dealer = blackJackGame.getDealer(); + return makePlayerStatusDto(dealer); + } + + private List makeChallengersStatus() { + List challengers = blackJackGame.getChallengers(); + return challengers.stream() + .map(challenger -> makePlayerStatusDto(challenger)) + .collect(Collectors.toUnmodifiableList()); + } + + private void takeTurn() { + try { + takeAllChallengersTurn(); + takeDealerTurn(); + } catch (CustomException e) { + OutputView.printErrorMessage(e); + } + } + + private void takeAllChallengersTurn() { + for (Player player : blackJackGame.getChallengers()) { + takeEachChallengerTurn(player); + } + } + + private void takeEachChallengerTurn(Player player) { + if (player.canPick()) { + checkChoice(player); + } + } + + private void checkChoice(Player player) { + try { + inputChoice(player); + } catch (CustomException e) { + OutputView.printErrorMessage(e); + checkChoice(player); + } + } + + private void inputChoice(Player player) { + boolean choice = InputView.inputPlayerChoice(player.getName()); + if (choice) { + blackJackGame.hit(player); + OutputView.printChallengerStatusInGame(makePlayerStatusDto(player)); + takeEachChallengerTurn(player); + } + } + + private void takeDealerTurn() { + Player dealer = blackJackGame.getDealer(); + boolean dealerCanPick = dealer.canPick(); + if (dealerCanPick) { + blackJackGame.hit(dealer); + } + OutputView.printDealerTurnResult(dealerCanPick, Dealer.MAXIMUM_POINT); + } + + private void showResult() { + showPoint(); + showRank(); + } + + private void showPoint() { + PlayerStatusDto dealerStatus = makeDealerStatus(); + List challengersStatus = makeChallengersStatus(); + OutputView.printEndStatus(dealerStatus, challengersStatus); + } + + private void showRank() { + ResultMap resultMap = blackJackGame.makeResult(); + + ChallengerResultDto challengerResultDto = makeChallengerResultDto(resultMap, blackJackGame.getChallengers()); + DealerResultDto dealerResultDto = makeDealerResultDto(resultMap, blackJackGame.getDealer()); + OutputView.printEndRank(challengerResultDto, dealerResultDto); + } + + private PlayerStatusDto makePlayerStatusDto(Player player) { + String playerName = player.getName(); + List inputCards = player.getHoldingCards().getCards(); + int playerPoint = player.getTotalPoint(); + return new PlayerStatusDto(playerName, makeCardInfo(inputCards), playerPoint); + } + + private static List makeCardInfo(List inputCards) { + List cardInfo = new ArrayList<>(); + for (Card card : inputCards) { + cardInfo.add(card.getSymbol().getName() + card.getShape().getName()); + } + return cardInfo; + } + + private ChallengerResultDto makeChallengerResultDto(ResultMap resultMap, List challengers) { + Map nameAndResult = new LinkedHashMap<>(); + for (Player challenger : challengers) { + ResultType challengerResultType = resultMap.getChallengerResult(challenger); + nameAndResult.put(challenger.getName(), challengerResultType.getLabel()); + } + return new ChallengerResultDto(nameAndResult); + } + + private DealerResultDto makeDealerResultDto(ResultMap resultMap, Player dealer) { + String dealerName = dealer.getName(); + Map dealerResult = resultMap.getDealerResult(); + int winCount = dealerResult.getOrDefault(ResultType.WIN, 0); + int drawCount = dealerResult.getOrDefault(ResultType.DRAW, 0); + int loseCount = dealerResult.getOrDefault(ResultType.LOSE, 0); + return new DealerResultDto(dealerName, winCount, drawCount, loseCount); + } +} diff --git a/src/main/java/blackjack/domain/BlackJackGame.java b/src/main/java/blackjack/domain/BlackJackGame.java new file mode 100644 index 00000000000..7ea4fa8759f --- /dev/null +++ b/src/main/java/blackjack/domain/BlackJackGame.java @@ -0,0 +1,45 @@ +package blackjack.domain; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardDeck; +import blackjack.domain.player.Player; +import blackjack.domain.player.Players; +import blackjack.domain.result.ResultMap; +import java.util.List; + +public class BlackJackGame { + private final CardDeck cardDeck; + private final Players players; + + private BlackJackGame(CardDeck cardDeck, Players players) { + this.cardDeck = cardDeck; + this.players = players; + } + + public static BlackJackGame from(List names) { + CardDeck cardDeck = CardDeck.create(); + Players players = Players.from(names); + return new BlackJackGame(cardDeck, players); + } + + public void handOutStartCards() { + players.pickStartCards(cardDeck); + } + + public void hit(Player player) { + Card pickedCard = cardDeck.pick(); + player.pickCard(pickedCard); + } + + public ResultMap makeResult() { + return ResultMap.from(players); + } + + public Player getDealer() { + return players.getDealer(); + } + + public List getChallengers() { + return players.getChallengers(); + } +} 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..1924a2ba193 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,28 @@ +package blackjack.domain.card; + +public class Card { + + private final Shape shape; + private final Symbol symbol; + + public Card(Shape shape, Symbol symbol) { + this.shape = shape; + this.symbol = symbol; + } + + public boolean isAce() { + return symbol.equals(Symbol.ACE); + } + + public Shape getShape() { + return shape; + } + + public Symbol getSymbol() { + return symbol; + } + + public int getPoint() { + return symbol.getValue(); + } +} diff --git a/src/main/java/blackjack/domain/card/CardDeck.java b/src/main/java/blackjack/domain/card/CardDeck.java new file mode 100644 index 00000000000..bfb6b9a9872 --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardDeck.java @@ -0,0 +1,38 @@ +package blackjack.domain.card; + +import static java.util.stream.Collectors.toList; + +import blackjack.domain.card.exception.NoMoreCardException; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +public class CardDeck { + + private final Deque cards; + + private CardDeck(Deque cards) { + this.cards = cards; + } + + public static CardDeck create() { + List cards = Arrays.stream(Shape.values()) + .flatMap(shape -> Arrays.stream(Symbol.values()).map(symbol -> new Card(shape, symbol))) + .collect(toList()); + Collections.shuffle(cards); + return new CardDeck(new ArrayDeque<>(cards)); + } + + public Card pick() { + validateCardExist(); + return cards.remove(); + } + + private void validateCardExist() { + if (cards.isEmpty()) { + throw new NoMoreCardException(); + } + } +} 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..9564da39cd3 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Shape.java @@ -0,0 +1,18 @@ +package blackjack.domain.card; + +public enum Shape { + SPADE("스페이드"), + HEART("하트"), + CLOVER("클로버"), + DIAMOND("다이아몬드"); + + private final String name; + + Shape(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/card/Symbol.java b/src/main/java/blackjack/domain/card/Symbol.java new file mode 100644 index 00000000000..e09ac7851c5 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Symbol.java @@ -0,0 +1,33 @@ +package blackjack.domain.card; + +public enum Symbol { + ACE("A", 1), + 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; + + Symbol(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/exception/NoMoreCardException.java b/src/main/java/blackjack/domain/card/exception/NoMoreCardException.java new file mode 100644 index 00000000000..f47cdd05bec --- /dev/null +++ b/src/main/java/blackjack/domain/card/exception/NoMoreCardException.java @@ -0,0 +1,12 @@ +package blackjack.domain.card.exception; + +import blackjack.common.exception.CustomException; + +public class NoMoreCardException extends CustomException { + + private static final String MESSAGE = "더 이상 뽑을 카드가 없습니다. 결과 창으로 넘어갑니다."; + + public NoMoreCardException() { + super(MESSAGE); + } +} diff --git a/src/main/java/blackjack/domain/player/Challenger.java b/src/main/java/blackjack/domain/player/Challenger.java new file mode 100644 index 00000000000..e7065376f2e --- /dev/null +++ b/src/main/java/blackjack/domain/player/Challenger.java @@ -0,0 +1,34 @@ +package blackjack.domain.player; + +import blackjack.domain.player.exception.InvalidPlayerNameException; + +public class Challenger extends Player { + + private final String name; + + public Challenger(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (Dealer.NAME.equals(name)) { + throw new InvalidPlayerNameException(); + } + } + + @Override + public Boolean canPick() { + return holdingCards.getSum() <= BLACKJACK_POINT; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isDealer() { + return false; + } +} diff --git a/src/main/java/blackjack/domain/player/Dealer.java b/src/main/java/blackjack/domain/player/Dealer.java new file mode 100644 index 00000000000..9c694c6d733 --- /dev/null +++ b/src/main/java/blackjack/domain/player/Dealer.java @@ -0,0 +1,50 @@ +package blackjack.domain.player; + +import blackjack.domain.result.ResultType; + +public class Dealer extends Player { + + public static final String NAME = "딜러"; + + public static final int MAXIMUM_POINT = 16; + + public ResultType judge(Player challenger) { + if (isBust() && challenger.isBust()) { + return ResultType.DRAW; + } + if (isBust() && !challenger.isBust()) { + return ResultType.WIN; + } + if (challenger.isBust()) { + return ResultType.LOSE; + } + return comparePoint(challenger); + } + + private ResultType comparePoint(Player challenger) { + int dealerPoint = this.getTotalPoint(); + int challengerPoint = challenger.getTotalPoint(); + if (challengerPoint < dealerPoint) { + return ResultType.LOSE; + } + if (challengerPoint > dealerPoint) { + return ResultType.WIN; + } + return ResultType.DRAW; + } + + @Override + public Boolean canPick() { + return holdingCards.getSum() <= MAXIMUM_POINT; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean isDealer() { + return true; + } +} diff --git a/src/main/java/blackjack/domain/player/HoldingCards.java b/src/main/java/blackjack/domain/player/HoldingCards.java new file mode 100644 index 00000000000..7520bc7f27f --- /dev/null +++ b/src/main/java/blackjack/domain/player/HoldingCards.java @@ -0,0 +1,48 @@ +package blackjack.domain.player; + +import blackjack.domain.card.Card; +import java.util.ArrayList; +import java.util.List; + +public class HoldingCards { + + private static final int MAXIMUM_SUM = 21; + private static final int ACE_BONUS = 10; + + private final List cards = new ArrayList<>(); + + public void initialize(Card firstCard, Card secondCard) { + cards.addAll(List.of(firstCard, secondCard)); + } + + public void add(Card card) { + cards.add(card); + } + + public List getCards() { + return List.copyOf(cards); + } + + public int getSum() { + int aceCount = getAceCount(); + int pointSum = getNotAceSum() + aceCount; + while (pointSum + ACE_BONUS <= MAXIMUM_SUM && aceCount > 0) { + pointSum += ACE_BONUS; + aceCount--; + } + return pointSum; + } + + private int getNotAceSum() { + return cards.stream() + .filter(card -> !card.isAce()) + .mapToInt(Card::getPoint) + .sum(); + } + + private int getAceCount() { + return (int) cards.stream() + .filter(Card::isAce) + .count(); + } +} diff --git a/src/main/java/blackjack/domain/player/Player.java b/src/main/java/blackjack/domain/player/Player.java new file mode 100644 index 00000000000..337da5e77ab --- /dev/null +++ b/src/main/java/blackjack/domain/player/Player.java @@ -0,0 +1,42 @@ +package blackjack.domain.player; + +import blackjack.domain.card.Card; + +public abstract class Player { + protected static final int BLACKJACK_POINT = 21; + + protected final HoldingCards holdingCards; + + protected Player() { + this.holdingCards = new HoldingCards(); + } + + public void pickStartCards(Card firstCard, Card secondCard) { + holdingCards.initialize(firstCard, secondCard); + } + + public void pickCard(Card card) { + holdingCards.add(card); + } + + public boolean isBust() { + if (holdingCards.getSum() > BLACKJACK_POINT) { + return true; + } + return false; + } + + public int getTotalPoint() { + return holdingCards.getSum(); + } + + public HoldingCards getHoldingCards() { + return holdingCards; + } + + public abstract Boolean canPick(); + + public abstract String getName(); + + public abstract boolean isDealer(); +} diff --git a/src/main/java/blackjack/domain/player/Players.java b/src/main/java/blackjack/domain/player/Players.java new file mode 100644 index 00000000000..77006487165 --- /dev/null +++ b/src/main/java/blackjack/domain/player/Players.java @@ -0,0 +1,58 @@ +package blackjack.domain.player; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardDeck; +import blackjack.domain.player.exception.DuplicatedPlayerNameException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Players { + + private final List players; + + private Players(List players) { + this.players = players; + } + + public static Players from(List names) { + validateDuplicatedNames(names); + List players = new ArrayList<>(); + players.add(new Dealer()); + names.stream() + .map(Challenger::new) + .forEach(players::add); + return new Players(players); + } + + private static void validateDuplicatedNames(List names) { + long distinctNameCount = names.stream() + .distinct() + .count(); + + if (names.size() != distinctNameCount) { + throw new DuplicatedPlayerNameException(); + } + } + + public void pickStartCards(CardDeck cardDeck) { + for (Player player : players) { + Card firstCard = cardDeck.pick(); + Card secondCard = cardDeck.pick(); + player.pickStartCards(firstCard, secondCard); + } + } + + public List getChallengers() { + return players.stream() + .filter(player -> !player.isDealer()) + .collect(Collectors.toUnmodifiableList()); + } + + public Player getDealer() { + return players.stream() + .filter(player -> player.isDealer()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("딜러가 존재하지 않습니다.")); + } +} diff --git a/src/main/java/blackjack/domain/player/exception/DuplicatedPlayerNameException.java b/src/main/java/blackjack/domain/player/exception/DuplicatedPlayerNameException.java new file mode 100644 index 00000000000..11e6eb9cf37 --- /dev/null +++ b/src/main/java/blackjack/domain/player/exception/DuplicatedPlayerNameException.java @@ -0,0 +1,12 @@ +package blackjack.domain.player.exception; + +import blackjack.common.exception.CustomException; + +public class DuplicatedPlayerNameException extends CustomException { + + private static final String MESSAGE = "중복된 이름입니다."; + + public DuplicatedPlayerNameException() { + super(MESSAGE); + } +} diff --git a/src/main/java/blackjack/domain/player/exception/InvalidPlayerNameException.java b/src/main/java/blackjack/domain/player/exception/InvalidPlayerNameException.java new file mode 100644 index 00000000000..926d7ea34fd --- /dev/null +++ b/src/main/java/blackjack/domain/player/exception/InvalidPlayerNameException.java @@ -0,0 +1,13 @@ +package blackjack.domain.player.exception; + +import blackjack.common.exception.CustomException; +import blackjack.domain.player.Dealer; + +public class InvalidPlayerNameException extends CustomException { + + private static final String MESSAGE = "플레이어의 이름은 " + Dealer.NAME + "이면 안됩니다."; + + public InvalidPlayerNameException() { + super(MESSAGE); + } +} diff --git a/src/main/java/blackjack/domain/result/ResultMap.java b/src/main/java/blackjack/domain/result/ResultMap.java new file mode 100644 index 00000000000..5b9af5d9b5f --- /dev/null +++ b/src/main/java/blackjack/domain/result/ResultMap.java @@ -0,0 +1,41 @@ +package blackjack.domain.result; + +import blackjack.domain.player.Dealer; +import blackjack.domain.player.Player; +import blackjack.domain.player.Players; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ResultMap { + private final Map results; + + private ResultMap(Map results) { + this.results = results; + } + + public static ResultMap from(Players players) { + Map results = new LinkedHashMap<>(); + Dealer dealer = (Dealer) players.getDealer(); + List challengers = players.getChallengers(); + for (Player challenger : challengers) { + ResultType resultType = dealer.judge(challenger); + results.put(challenger, resultType); + } + return new ResultMap(results); + } + + public ResultType getChallengerResult(Player player) { + return results.get(player); + } + + public Map getDealerResult() { + Map dealerResult = new HashMap<>(); + for (ResultType resultType : results.values()) { + ResultType dealerRank = resultType.getOpposite(); + dealerResult.put(dealerRank, dealerResult.getOrDefault(dealerRank, 0) + 1); + } + return dealerResult; + } +} diff --git a/src/main/java/blackjack/domain/result/ResultType.java b/src/main/java/blackjack/domain/result/ResultType.java new file mode 100644 index 00000000000..94c3b61ddf6 --- /dev/null +++ b/src/main/java/blackjack/domain/result/ResultType.java @@ -0,0 +1,28 @@ +package blackjack.domain.result; + +public enum ResultType { + + WIN("승"), + DRAW("무"), + LOSE("패"); + + private final String label; + + ResultType(String label) { + this.label = label; + } + + public ResultType getOpposite() { + if (this.equals(ResultType.WIN)) { + return LOSE; + } + if (this.equals(ResultType.LOSE)) { + return WIN; + } + return DRAW; + } + + public String getLabel() { + return label; + } +} diff --git a/src/main/java/blackjack/dto/ChallengerResultDto.java b/src/main/java/blackjack/dto/ChallengerResultDto.java new file mode 100644 index 00000000000..2a818ba58fb --- /dev/null +++ b/src/main/java/blackjack/dto/ChallengerResultDto.java @@ -0,0 +1,16 @@ +package blackjack.dto; + +import java.util.Map; + +public class ChallengerResultDto { + + private final Map nameAndResult; + + public ChallengerResultDto(Map nameAndResult) { + this.nameAndResult = nameAndResult; + } + + public Map getNameAndResult() { + return nameAndResult; + } +} diff --git a/src/main/java/blackjack/dto/DealerResultDto.java b/src/main/java/blackjack/dto/DealerResultDto.java new file mode 100644 index 00000000000..6f877541ce7 --- /dev/null +++ b/src/main/java/blackjack/dto/DealerResultDto.java @@ -0,0 +1,32 @@ +package blackjack.dto; + +public class DealerResultDto { + + private final String name; + private final int winCount; + private final int drawCount; + private final int loseCount; + + public DealerResultDto(String name, int winCount, int drawCount, int loseCount) { + this.name = name; + this.winCount = winCount; + this.drawCount = drawCount; + this.loseCount = loseCount; + } + + public String getName() { + return name; + } + + public int getWinCount() { + return winCount; + } + + public int getDrawCount() { + return drawCount; + } + + public int getLoseCount() { + return loseCount; + } +} diff --git a/src/main/java/blackjack/dto/PlayerStatusDto.java b/src/main/java/blackjack/dto/PlayerStatusDto.java new file mode 100644 index 00000000000..54198aa8188 --- /dev/null +++ b/src/main/java/blackjack/dto/PlayerStatusDto.java @@ -0,0 +1,28 @@ +package blackjack.dto; + +import java.util.List; + +public class PlayerStatusDto { + + private final String name; + private final List cards; + private final int point; + + public PlayerStatusDto(String name, List cards, int point) { + this.name = name; + this.cards = cards; + this.point = point; + } + + public String getName() { + return name; + } + + public List getCards() { + return cards; + } + + public int getPoint() { + return point; + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 00000000000..332a06813f4 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,53 @@ +package blackjack.view; + +import blackjack.view.exception.InvalidChoiceException; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + + private static final String DELIMITER = ","; + private static final String YES = "y"; + private static final String NO = "n"; + private static final String REQUEST_PLAYER_NAME = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"; + private static final String REQUEST_PLAYER_CHOICE = "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"; + + public static List inputPlayerNames() { + System.out.println(REQUEST_PLAYER_NAME); + + String playerNames = scanner.nextLine(); + return splitInputByDelimiter(playerNames); + } + + private static List splitInputByDelimiter(String input) { + return Arrays.stream(input.split(DELIMITER)) + .map(String::trim) + .collect(Collectors.toUnmodifiableList()); + } + + public static boolean inputPlayerChoice(String name) { + System.out.println(); + System.out.println(name + REQUEST_PLAYER_CHOICE); + String choice = scanner.nextLine(); + return validateChoice(choice); + } + + private static boolean validateChoice(String choice) { + String lowerCase = choice.toLowerCase(); + if (lowerCase.equals(YES)) { + return true; + } + if (lowerCase.equals(NO)) { + return false; + } + throw new InvalidChoiceException(); + } + + public static void terminate() { + scanner.close(); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 00000000000..8717fa5dd75 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,134 @@ +package blackjack.view; + +import blackjack.dto.ChallengerResultDto; +import blackjack.dto.DealerResultDto; +import blackjack.dto.PlayerStatusDto; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String GIVE_START_CARD_COMPLETE_MESSAGE = "에게 2장을 나누었습니다."; + private static final String DEALER_CAN_PICK_MESSAGE = "딜러는 %d이하라 한장의 카드를 더 받았습니다."; + private static final String DEALER_CAN_NOT_PICK_MESSAGE = "딜러는 %d이상이라 한장의 카드를 더 받지 못했습니다."; + private static final String FINAL_RESULT_HEADER_MESSAGE = "## 최종 승패"; + private static final String CARD = "카드"; + private static final String ITEM_DELIMITER = ", "; + private static final String PLAYER_NAME_AND_CARDS_PARTITION = ": "; + private static final String RESULT_PREFIX = " - 결과: "; + + public static void printErrorMessage(Exception exception) { + System.out.println(exception.getMessage()); + } + + public static void printStartStatus(PlayerStatusDto dealerStatus, List challengersStatus) { + System.out.println(); + printGivenMessage(dealerStatus, challengersStatus); + printDealerStatus(dealerStatus); + System.out.println(); + printChallengersStatus(challengersStatus); + } + + private static void printGivenMessage(PlayerStatusDto dealerStatus, List challengersStatus) { + System.out.print(dealerStatus.getName() + "와 "); + String challengerNames = String.join(ITEM_DELIMITER, toChallengerNames(challengersStatus)); + System.out.print(challengerNames); + System.out.println(GIVE_START_CARD_COMPLETE_MESSAGE); + } + + public static void printDealerStatus(PlayerStatusDto dealerStatus) { + System.out.print(dealerStatus.getName()); + System.out.print(PLAYER_NAME_AND_CARDS_PARTITION); + String cards = String.join(ITEM_DELIMITER, dealerStatus.getCards()); + System.out.print(cards); + } + + public static void printChallengersStatus(List challengersStatus) { + for (PlayerStatusDto challenger : challengersStatus) { + printChallengerStatusInGame(challenger); + } + } + + public static void printChallengerStatusInGame(PlayerStatusDto playerStatusDto) { + printChallengerStatus(playerStatusDto); + System.out.println(); + } + + public static void printChallengerStatus(PlayerStatusDto challenger) { + System.out.print(challenger.getName() + CARD); + System.out.print(PLAYER_NAME_AND_CARDS_PARTITION); + String cards = String.join(ITEM_DELIMITER, challenger.getCards()); + System.out.print(cards); + } + + private static List toChallengerNames(List challengersStatus) { + return challengersStatus.stream() + .map(PlayerStatusDto::getName) + .collect(Collectors.toUnmodifiableList()); + } + + public static void printDealerTurnResult(boolean dealerCanPick, int dealerMaximumPoint) { + System.out.println(); + if (dealerCanPick) { + System.out.println(String.format(DEALER_CAN_PICK_MESSAGE, dealerMaximumPoint)); + System.out.println(); + return; + } + System.out.println(String.format(DEALER_CAN_NOT_PICK_MESSAGE, dealerMaximumPoint + 1)); + System.out.println(); + } + + public static void printEndStatus(PlayerStatusDto dealerStatus, List challengersStatus) { + printDealerStatus(dealerStatus); + printPoint(dealerStatus.getPoint()); + for (PlayerStatusDto challenger : challengersStatus) { + printChallengerStatus(challenger); + printPoint(challenger.getPoint()); + } + } + + private static void printPoint(int point) { + System.out.println(RESULT_PREFIX + point); + } + + public static void printEndRank(ChallengerResultDto challengerResultDto, DealerResultDto dealerResultDto) { + System.out.println(); + System.out.println(FINAL_RESULT_HEADER_MESSAGE); + printDealerFinalRank(dealerResultDto); + printChallengersFinalRank(challengerResultDto); + } + + private static void printDealerFinalRank(DealerResultDto dealerResultDto) { + System.out.print(dealerResultDto.getName() + PLAYER_NAME_AND_CARDS_PARTITION); + printDealerWinCount(dealerResultDto); + printDealerDrawCount(dealerResultDto); + printDealerLoseCount(dealerResultDto); + System.out.println(); + } + + private static void printDealerWinCount(DealerResultDto dealerResultDto) { + if (dealerResultDto.getWinCount() != 0) { + System.out.print(dealerResultDto.getWinCount() + "승 "); + } + } + + private static void printDealerDrawCount(DealerResultDto dealerResultDto) { + if (dealerResultDto.getDrawCount() != 0) { + System.out.print(dealerResultDto.getDrawCount() + "무 "); + } + } + + private static void printDealerLoseCount(DealerResultDto dealerResultDto) { + if (dealerResultDto.getLoseCount() != 0) { + System.out.print(dealerResultDto.getLoseCount() + "패 "); + } + } + + private static void printChallengersFinalRank(ChallengerResultDto challengerResultDto) { + Map nameAndResult = challengerResultDto.getNameAndResult(); + for (String name : nameAndResult.keySet()) { + System.out.println(name + PLAYER_NAME_AND_CARDS_PARTITION + nameAndResult.get(name)); + } + } +} diff --git a/src/main/java/blackjack/view/exception/InvalidChoiceException.java b/src/main/java/blackjack/view/exception/InvalidChoiceException.java new file mode 100644 index 00000000000..3ba8baba84d --- /dev/null +++ b/src/main/java/blackjack/view/exception/InvalidChoiceException.java @@ -0,0 +1,12 @@ +package blackjack.view.exception; + +import blackjack.common.exception.CustomException; + +public class InvalidChoiceException extends CustomException { + + private static final String MESSAGE = "올바르지 않은 입력입니다. y 또는 n 중에 골라주세요."; + + public InvalidChoiceException() { + super(MESSAGE); + } +} diff --git a/src/test/java/blackjack/domain/card/CardDeckTest.java b/src/test/java/blackjack/domain/card/CardDeckTest.java new file mode 100644 index 00000000000..313f14da340 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardDeckTest.java @@ -0,0 +1,20 @@ +package blackjack.domain.card; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import blackjack.domain.card.exception.NoMoreCardException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardDeckTest { + + @Test + @DisplayName("카드 52장이 생성되는지 테스트") + void pick_random_card() { + CardDeck cardDeck = CardDeck.create(); + for (int i = 0; i < 52; i++) { + cardDeck.pick(); + } + assertThrows(NoMoreCardException.class, () -> cardDeck.pick()); + } +} diff --git a/src/test/java/blackjack/domain/card/CardTest.java b/src/test/java/blackjack/domain/card/CardTest.java new file mode 100644 index 00000000000..e6c83b7b904 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,16 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardTest { + + @Test + @DisplayName("카드의 숫자를 보고 값을 반환한다") + void get_value() { + Card card = new Card(Shape.SPADE, Symbol.FOUR); + assertThat(card.getPoint()).isEqualTo(4); + } +} diff --git a/src/test/java/blackjack/domain/player/ChallengerTest.java b/src/test/java/blackjack/domain/player/ChallengerTest.java new file mode 100644 index 00000000000..c4a58c2b06a --- /dev/null +++ b/src/test/java/blackjack/domain/player/ChallengerTest.java @@ -0,0 +1,54 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import blackjack.domain.player.exception.InvalidPlayerNameException; +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 ChallengerTest { + + @Test + @DisplayName("이름이 '딜러'인 경우 예외가 발생한다") + void validate_name() { + assertThrows(InvalidPlayerNameException.class, + () -> new Challenger("딜러")); + } + + @ParameterizedTest + @MethodSource("provideCards") + @DisplayName("카드를 뽑을 수 있는지 확인한다.") + void checking_sum_is_over_21(List cards, boolean expected) { + Player player = new Challenger("neo"); + for (Card card : cards) { + player.pickCard(card); + } + + assertThat(player.canPick()).isEqualTo(expected); + } + + private static Stream provideCards() { + return Stream.of( + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE)), + true), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE), + new Card(Shape.HEART, Symbol.EIGHT)), + false) + ); + } +} diff --git a/src/test/java/blackjack/domain/player/DealerTest.java b/src/test/java/blackjack/domain/player/DealerTest.java new file mode 100644 index 00000000000..484766d274b --- /dev/null +++ b/src/test/java/blackjack/domain/player/DealerTest.java @@ -0,0 +1,77 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import blackjack.domain.result.ResultType; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class DealerTest { + @ParameterizedTest + @MethodSource("challengerCards") + @DisplayName("다른 Challenger의 게임 결과를 올바르게 판단하는지 확인한다") + void judge(List cards, ResultType expected) { + Challenger challenger = new Challenger("oing"); + challenger.pickStartCards(cards.get(0), cards.get(1)); + + Dealer dealer = new Dealer(); + dealer.pickStartCards(new Card(Shape.HEART, Symbol.KING), new Card(Shape.SPADE, Symbol.QUEEN)); + + assertThat(dealer.judge(challenger)).isEqualTo(expected); + } + + private static Stream challengerCards() { + return Stream.of( + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.SIX)), + ResultType.LOSE), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.ACE), + new Card(Shape.CLOVER, Symbol.KING)), + ResultType.WIN), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.JACK), + new Card(Shape.CLOVER, Symbol.KING)), + ResultType.DRAW) + ); + } + + + @ParameterizedTest + @MethodSource("provideCards") + @DisplayName("카드를 뽑을 수 있는지 확인한다") + void checking_sum_is_over_16(List cards, boolean expected) { + Player player = new Dealer(); + for (Card card : cards) { + player.pickCard(card); + } + + assertThat(player.canPick()).isEqualTo(expected); + } + + private static Stream provideCards() { + return Stream.of( + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.SIX)), + true), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.SEVEN)), + false) + ); + } +} diff --git a/src/test/java/blackjack/domain/player/HoldingCardsTest.java b/src/test/java/blackjack/domain/player/HoldingCardsTest.java new file mode 100644 index 00000000000..3e693d400b0 --- /dev/null +++ b/src/test/java/blackjack/domain/player/HoldingCardsTest.java @@ -0,0 +1,57 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HoldingCardsTest { + + private HoldingCards holdingCards; + + @BeforeEach + void setup() { + holdingCards = new HoldingCards(); + } + + @ParameterizedTest + @MethodSource("provideCards") + @DisplayName("카드의 합") + void sum(List cards, int expected) { + for (Card card : cards) { + holdingCards.add(card); + } + assertThat(holdingCards.getSum()).isEqualTo(expected); + } + + private static Stream provideCards() { + return Stream.of( + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE)), + 15), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE), + new Card(Shape.HEART, Symbol.EIGHT)), + 23), + Arguments.of( + List.of( + new Card(Shape.CLOVER, Symbol.ACE), + new Card(Shape.SPADE, Symbol.ACE), + new Card(Shape.HEART, Symbol.ACE), + new Card(Shape.CLOVER, Symbol.FIVE)), + 18) + ); + } +} diff --git a/src/test/java/blackjack/domain/player/PlayerTest.java b/src/test/java/blackjack/domain/player/PlayerTest.java new file mode 100644 index 00000000000..f1bf7c195ee --- /dev/null +++ b/src/test/java/blackjack/domain/player/PlayerTest.java @@ -0,0 +1,96 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PlayerTest { + + private Player player; + + @BeforeEach + void setup() { + player = new Player() { + @Override + public Boolean canPick() { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean isDealer() { + return false; + } + }; + } + + + @Test + @DisplayName("플레이어는 초기 카드 2장을 받는다") + void start_with_two_cards() { + Card card1 = new Card(Shape.HEART, Symbol.FOUR); + Card card2 = new Card(Shape.CLOVER, Symbol.KING); + player.pickStartCards(card1, card2); + + assertThat(player.getHoldingCards().getCards()) + .containsExactly(card1, card2); + } + + @Test + @DisplayName("추가 카드를 뽑는다.") + void pick_card() { + Card card = new Card(Shape.DIAMOND, Symbol.JACK); + player.pickCard(card); + + assertThat(player.getHoldingCards().getCards()) + .contains(card); + } + + @ParameterizedTest + @MethodSource("provideCards") + @DisplayName("버스트인지 확인") + void is_bust(List cards, boolean expected) { + for (Card card : cards) { + player.pickCard(card); + } + + assertThat(player.isBust()).isEqualTo(expected); + } + + private static Stream provideCards() { + return Stream.of( + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE)), + false), + Arguments.of( + List.of( + new Card(Shape.DIAMOND, Symbol.QUEEN), + new Card(Shape.CLOVER, Symbol.FIVE), + new Card(Shape.HEART, Symbol.EIGHT)), + true), + Arguments.of( + List.of( + new Card(Shape.CLOVER, Symbol.ACE), + new Card(Shape.SPADE, Symbol.ACE), + new Card(Shape.HEART, Symbol.ACE), + new Card(Shape.CLOVER, Symbol.FIVE)), + false) + ); + } +} diff --git a/src/test/java/blackjack/domain/player/PlayersTest.java b/src/test/java/blackjack/domain/player/PlayersTest.java new file mode 100644 index 00000000000..754f3d8ebfc --- /dev/null +++ b/src/test/java/blackjack/domain/player/PlayersTest.java @@ -0,0 +1,38 @@ +package blackjack.domain.player; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import blackjack.domain.player.exception.DuplicatedPlayerNameException; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayersTest { + + @Test + @DisplayName("플레이어의 이름이 중복되면 예외가 발생한다") + void checking_player_name_duplicated() { + assertThrows(DuplicatedPlayerNameException.class, + () -> Players.from(List.of("pobi", "pobi"))); + } + + @Test + @DisplayName("challenger만 반환하는지 테스트") + void return_challengers() { + Players players = Players.from(List.of("pobi", "oing")); + List challengers = players.getChallengers(); + + challengers.forEach(challenger -> + assertThat(challenger).isInstanceOf(Challenger.class)); + } + + @Test + @DisplayName("dealer만 반환하는지 테스트") + void return_dealer() { + Players players = Players.from(List.of("pobi", "oing")); + Player dealer = players.getDealer(); + + assertThat(dealer).isInstanceOf(Dealer.class); + } +} diff --git a/src/test/java/blackjack/domain/result/ResultMapTest.java b/src/test/java/blackjack/domain/result/ResultMapTest.java new file mode 100644 index 00000000000..b5a4a190863 --- /dev/null +++ b/src/test/java/blackjack/domain/result/ResultMapTest.java @@ -0,0 +1,53 @@ +package blackjack.domain.result; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Shape; +import blackjack.domain.card.Symbol; +import blackjack.domain.player.Player; +import blackjack.domain.player.Players; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResultMapTest { + + private ResultMap resultMap; + private Players players; + + @BeforeEach + void setPlayers() { + players = Players.from(List.of("oing", "ditoo")); + + Card heartKing = new Card(Shape.HEART, Symbol.KING); + Card heartSeven = new Card(Shape.HEART, Symbol.SEVEN); + Card heartThree = new Card(Shape.HEART, Symbol.THREE); + + players.getDealer().pickCard(heartSeven); + players.getChallengers().get(0).pickCard(heartThree); + players.getChallengers().get(1).pickCard(heartKing); + resultMap = ResultMap.from(players); + } + + @Test + @DisplayName("도전자들의 승패가 올바르게 계산되는지 확인한다") + void check_challengers_result() { + Player oing = players.getChallengers().get(0); + Player ditoo = players.getChallengers().get(1); + + assertThat(resultMap.getChallengerResult(oing)).isEqualTo(ResultType.LOSE); + assertThat(resultMap.getChallengerResult(ditoo)).isEqualTo(ResultType.WIN); + } + + @Test + @DisplayName("딜러의 승패가 올바르게 계산되는지 확인한다") + void check_dealer_result() { + Map dealerResult = resultMap.getDealerResult(); + assertThat(dealerResult) + .containsOnly(entry(ResultType.WIN, 1), entry(ResultType.LOSE, 1)); + } +}