diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..b819b8e049 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,90 @@ +# 블랙잭 규칙 + +## 참여자 + +**승** + +* (딜러 카드 합 < 참여자 카드 합) && 참여자가 버스트가 아닌 경우 +* (딜러 카드 합 == 참여자 카드 합) && (딜러 카드 수 > 참여자 카드 수) && 참여자가 버스트가 아닌 경우 +* 딜러가 버스트인 경우 && 참여자가 버스트가 아닌 경우 + +**무** + +* (딜러 카드 합 == 참여자 카드 합) && (딜러 카드 수 == 참여자 카드 수) && 참여자가 버스트가 아닌 경우 + +**패** + +* 참여자가 버스트인 경우 +* (딜러 카드 합 > 참여자 카드 합) + +
+ +# 기능 요구 사항 + +* [x] 참여자 이름 입력 받는다. + * [x] 양 끝 공백을 제거한다. + * [x] 참여자는 쉼표(,)로 구분한다. + * [x] null 이나 공백인 경우 예외가 발생한다. + * [x] 쉼표로 시작시 예외가 발생한다. ex) ,pobi,jason + * [x] 쉼표로 끝날시 예외가 발생한다. ex) pobi,jason, + * [x] 쉼표가 연속으로 올시 예외가 발생한다. ex) pobi,,jason + * [x] 참여자 이름이 중복시 예외가 발생한다. +* [x] 총 참여자의 수는 2이상 8이하여야 한다. + +
+ +* [x] 딜러가 카드 2장을 분배하다. + * [x] 카드는 총 6벌을 둔다. (52 * 6) +* [x] 딜러의 카드를 출력한다. (1장) +* [x] 참여자의 카드를 출력한다. (2장) + +
+ +* [x] 참여자는 hit(y) / stay(n)를 선택한다. + * [x] y, n 가 아닐시 예외가 발생한다. +* [x] 참여자가 hit을 하는 경우 현재 가지고 있는 카드를 출력한다. +* [x] 참여자가 hit을 한 적 없이 stay를 하는 경우 현재 가지고 있는 카드를 출력한다. +* [x] 카드 합이 블랙잭인 경우 블랙잭 메시지를 출력한다. +* [x] 카드 합이 21 초과시 버스트 메시지를 출력한다. + +
+ +* [x] 딜러의 카드의 합을 계산한다. +* [x] 카드 내의 ACE 가 포함된 경우 + * ACE: 11 + * 11로 했을 때 카드의 합이 21을 초과한 경우 1로 계산 +* [x] 17 이상이 될때까지 카드를 받는다. + +
+ +* [x] 모든 참여자의 카드의 합을 계산한다. +* [x] 딜러의 승패무를 확인한다. +* [x] 참여자의 승패무를 확인한다. +* [x] 게임 결과를 출력한다. + +--- + +## 리팩터링 목록 + +~~추상 클래스 활용~~ + +* [x] 카드덱이 비었을때 꺼낸 경우 예외 발생 +* [x] createEmptyPacket 메서드명 수정 +* [x] 참가자의 이름 딜러 불가 +* [x] Name 객체 포장하기 +* [x] Result 함수형 프로그래밍 사용 +* [x] TestFixture 사용 +* [x] 패키지 정리 +* [x] 메서드 컨벤션 작성 및 확인 +* [x] final 컨벤션 통일 +* [x] CardDeck 생성자 닫기 + +
+ +* [x] 예외 시 재입력 기능 +* [x] 예외 메시지 상수화 + * 커스텀 예외 구현 +* [x] CardDeck 웨어하우스로 바라보기 +* [ ] ~~컨트롤러 메서드 수정하기~~ +* [x] ~~Participant 추상 클래스 대신 클래스로 변경하기~~ + * 추상 클래스를 유지하고 추상 메서드 추가 diff --git a/src/main/java/application/Application.java b/src/main/java/application/Application.java new file mode 100644 index 0000000000..2d98d55bfc --- /dev/null +++ b/src/main/java/application/Application.java @@ -0,0 +1,18 @@ +package application; + +import controller.BlackJackController; +import controller.InputController; +import java.util.Scanner; +import view.InputView; +import view.OutputView; + +public class Application { + public static void main(String[] args) { + final Scanner scanner = new Scanner(System.in); + final InputView inputView = new InputView(scanner); + final OutputView outputView = new OutputView(); + final InputController inputController = new InputController(inputView, outputView); + final BlackJackController blackJackController = new BlackJackController(inputController, outputView); + blackJackController.run(); + } +} diff --git a/src/main/java/constants/ErrorCode.java b/src/main/java/constants/ErrorCode.java new file mode 100644 index 0000000000..d6278bf1c5 --- /dev/null +++ b/src/main/java/constants/ErrorCode.java @@ -0,0 +1,15 @@ +package constants; + +public enum ErrorCode { + + NOT_EXIST_MESSAGE, + INVALID_SEPARATOR, + INVALID_INPUT, + INVALID_SIZE, + DUPLICATE_NAME, + RESERVED_NAME, + BLANK_VALUE, + EMPTY_CARD, + INVALID_COMMAND, + ; +} diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java new file mode 100644 index 0000000000..e44b17ed96 --- /dev/null +++ b/src/main/java/controller/BlackJackController.java @@ -0,0 +1,92 @@ +package controller; + +import domain.Answer; +import domain.participant.Dealer; +import domain.participant.Player; +import domain.participant.Players; +import dto.DealerHandsDto; +import dto.ParticipantDto; +import dto.ParticipantsDto; +import view.OutputView; + +public class BlackJackController { + + private final InputController inputController; + private final OutputView outputView; + + + public BlackJackController(final InputController inputController, final OutputView outputView) { + this.inputController = inputController; + this.outputView = outputView; + } + + public void run() { + final Players players = inputController.getPlayers(); + final Dealer dealer = new Dealer(); + + initHands(players, dealer); + dealWithPlayers(players, dealer); + + if (!players.isAllBust()) { + dealer.deal(); + printDealerTurnMessage(dealer.countAddedHands()); + } + + printFinalResult(players, dealer); + } + + private void printDealerTurnMessage(final int turn) { + for (int i = 0; i < turn; i++) { + outputView.printDealerTurnMessage(); + } + } + + private void dealWithPlayers(final Players players, final Dealer dealer) { + for (Player player : players.getPlayers()) { + deal(player, dealer); + } + } + + private void initHands(final Players players, final Dealer dealer) { + dealer.initHands(players); + outputView.printStartDeal(DealerHandsDto.from(dealer), ParticipantsDto.of(players)); + } + + private void printFinalResult(final Players players, final Dealer dealer) { + outputView.printHandsResult(ParticipantsDto.of(dealer, players)); + outputView.printGameResult(dealer.getDealerResult(players), players.getPlayersResult(dealer)); + } + + private void deal(final Player player, final Dealer dealer) { + boolean handsChanged = false; + boolean turnEnded = false; + + while (!turnEnded) { + final Answer answer = inputController.getAnswer(player.getName()); + dealer.deal(player, answer); + + printHandsIfRequired(player, handsChanged, answer); + + handsChanged = true; + turnEnded = isTurnEnded(player, answer); + } + } + + private void printHandsIfRequired(final Player player, final boolean handsChanged, final Answer answer) { + if (shouldShowHands(handsChanged, answer)) { + outputView.printHands(ParticipantDto.from(player)); + } + } + + private boolean isTurnEnded(final Player player, final Answer answer) { + if (player.canDeal()) { + return !answer.isHit(); + } + outputView.printDealEndMessage(player.isBust()); + return true; + } + + private boolean shouldShowHands(final boolean handsChanged, final Answer answer) { + return answer.isHit() || !handsChanged; + } +} diff --git a/src/main/java/controller/InputController.java b/src/main/java/controller/InputController.java new file mode 100644 index 0000000000..4f9bb5df96 --- /dev/null +++ b/src/main/java/controller/InputController.java @@ -0,0 +1,55 @@ +package controller; + +import domain.Answer; +import domain.participant.Players; +import exception.CustomException; +import java.util.List; +import view.InputView; +import view.OutputView; + +public class InputController { + + private final InputView inputView; + private final OutputView outputView; + + public InputController(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public Players getPlayers() { + Players players; + do { + players = readPlayers(); + } while (players == null); + return players; + } + + public Answer getAnswer(String name) { + Answer answer; + do { + answer = readAnswer(name); + } while (answer == null); + return answer; + } + + private Players readPlayers() { + try { + List rawNames = inputView.readNames(); + return Players.from(rawNames); + } catch (CustomException exception) { + outputView.printException(exception.getErrorCode()); + return null; + } + } + + private Answer readAnswer(String name) { + try { + String value = inputView.readAnswer(name); + return Answer.from(value); + } catch (CustomException exception) { + outputView.printException(exception.getErrorCode()); + return null; + } + } +} diff --git a/src/main/java/domain/Answer.java b/src/main/java/domain/Answer.java new file mode 100644 index 0000000000..dc53bfc33a --- /dev/null +++ b/src/main/java/domain/Answer.java @@ -0,0 +1,28 @@ +package domain; + +import constants.ErrorCode; +import exception.InvalidCommandException; +import java.util.Arrays; + +public enum Answer { + + HIT("y"), + STAY("n"); + + private final String value; + + Answer(final String value) { + this.value = value; + } + + public static Answer from(final String value) { + return Arrays.stream(Answer.values()) + .filter(answer -> answer.value.equals(value)) + .findFirst() + .orElseThrow(() -> new InvalidCommandException(ErrorCode.INVALID_COMMAND)); + } + + public boolean isHit() { + return HIT.equals(this); + } +} diff --git a/src/main/java/domain/Result.java b/src/main/java/domain/Result.java new file mode 100644 index 0000000000..15cdb94994 --- /dev/null +++ b/src/main/java/domain/Result.java @@ -0,0 +1,55 @@ +package domain; + +import domain.participant.Hands; +import java.util.Arrays; +import java.util.function.BiPredicate; + +public enum Result { + + WIN("승", Result::winningCondition), + TIE("무", Result::tieCondition), + LOSE("패", Result::loseCondition); + + private final String value; + private final BiPredicate condition; + + Result(final String value, final BiPredicate condition) { + this.value = value; + this.condition = condition; + } + + public Result reverse() { + if (Result.WIN.equals(this)) { + return LOSE; + } + if (Result.LOSE.equals(this)) { + return WIN; + } + return TIE; + } + + public static Result calculateOf(final Hands hands, final Hands target) { + return Arrays.stream(Result.values()) + .filter(result -> result.condition.test(hands, target)) + .findFirst() + .orElseThrow(); + } + + private static boolean winningCondition(final Hands hands, final Hands target) { + return (!hands.isBust() && target.isBust()) + || (hands.sum() > target.sum() && !hands.isBust()) + || (hands.sum() == target.sum() && hands.size() < target.size() && !hands.isBust()); + } + + private static boolean tieCondition(final Hands hands, final Hands target) { + return hands.sum() == target.sum() && hands.size() == target.size() && !hands.isBust(); + } + + private static boolean loseCondition(final Hands hands, final Hands target) { + return hands.isBust() || hands.sum() < target.sum() || !target.isBust(); + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 0000000000..f32cdd138f --- /dev/null +++ b/src/main/java/domain/card/Card.java @@ -0,0 +1,63 @@ +package domain.card; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Card { + + private static final List CACHE = new ArrayList<>(); + + static { + for (Shape shape : Shape.values()) { + generateCard(shape); + } + } + + private final Rank rank; + private final Shape shape; + + public Card(final Rank rank, final Shape shape) { + this.rank = rank; + this.shape = shape; + } + + private static void generateCard(final Shape shape) { + for (Rank rank : Rank.values()) { + CACHE.add(new Card(rank, shape)); + } + } + + public static List values() { + return List.copyOf(CACHE); + } + + public boolean isAce() { + return rank.isAce(); + } + + public int getCardNumber() { + return rank.getValue(); + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + if (!(target instanceof Card card)) { + return false; + } + return Objects.equals(rank, card.rank) && Objects.equals(shape, card.shape); + } + + @Override + public int hashCode() { + return Objects.hash(rank, shape); + } + + @Override + public String toString() { + return rank.getName() + shape.getName(); + } +} diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java new file mode 100644 index 0000000000..90432e4fa5 --- /dev/null +++ b/src/main/java/domain/card/CardDeck.java @@ -0,0 +1,39 @@ +package domain.card; + +import constants.ErrorCode; +import exception.NoMoreCardException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +public class CardDeck { + + private final Deque cards; + + public CardDeck(final Deque cards) { + this.cards = cards; + } + + protected static CardDeck generate(int size) { + final List deck = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + deck.addAll(Card.values()); + } + Collections.shuffle(deck); + return new CardDeck(new ArrayDeque<>(deck)); + } + + public Card pop() { + if (cards.isEmpty()) { + throw new NoMoreCardException(ErrorCode.EMPTY_CARD); + } + return cards.pop(); + } + + public int size() { + return cards.size(); + } +} diff --git a/src/main/java/domain/card/Rank.java b/src/main/java/domain/card/Rank.java new file mode 100644 index 0000000000..aa92ef3465 --- /dev/null +++ b/src/main/java/domain/card/Rank.java @@ -0,0 +1,38 @@ +package domain.card; + +public enum Rank { + + 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; + + Rank(final String name, final int value) { + this.name = name; + this.value = value; + } + + public boolean isAce() { + return this == ACE; + } + + public int getValue() { + return value; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/domain/card/Shape.java b/src/main/java/domain/card/Shape.java new file mode 100644 index 0000000000..093d73cfa8 --- /dev/null +++ b/src/main/java/domain/card/Shape.java @@ -0,0 +1,19 @@ +package domain.card; + +public enum Shape { + + SPADE("스페이드"), + HEART("하트"), + CLOVER("클로버"), + DIAMOND("다이아몬드"); + + private final String name; + + Shape(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java new file mode 100644 index 0000000000..08cc130792 --- /dev/null +++ b/src/main/java/domain/participant/Dealer.java @@ -0,0 +1,79 @@ +package domain.participant; + +import domain.Answer; +import domain.Result; +import domain.card.Card; +import domain.card.CardDeck; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class Dealer extends Participant { + + public static final int INIT_HANDS_SIZE = 2; + public static final int THRESHOLD = 16; + public static final int DECK_SIZE = 6; + public static final Name DEALER_NAME = new Name("딜러"); + + private final CardDeck cardDeck; + + public Dealer() { + super(DEALER_NAME, Hands.createEmptyHands()); + this.cardDeck = new CardDeck(generate()); + } + + public Dealer(final Hands hands) { + super(DEALER_NAME, hands); + this.cardDeck = new CardDeck(generate()); + } + + private static ArrayDeque generate() { + final List deck = new ArrayList<>(); + for (int i = 0; i < DECK_SIZE; i++) { + deck.addAll(Card.values()); + } + Collections.shuffle(deck); + return new ArrayDeque<>(deck); + } + + public void initHands(final Players players) { + for (int i = 0; i < INIT_HANDS_SIZE; i++) { + players.forEach(player -> player.add(cardDeck.pop())); + super.add(cardDeck.pop()); + } + } + + public void deal(final Player player, final Answer answer) { + if (answer.isHit()) { + player.add(cardDeck.pop()); + } + } + + public void deal() { + while (canDeal()) { + super.add(cardDeck.pop()); + } + } + + public int countAddedHands() { + return handsSize() - INIT_HANDS_SIZE; + } + + public Map getDealerResult(final Players players) { + Map dealerResult = new EnumMap<>(Result.class); + + for (Result value : players.getPlayersResult(this).values()) { + Result reversed = value.reverse(); + dealerResult.put(reversed, dealerResult.getOrDefault(reversed, 0) + 1); + } + return dealerResult; + } + + @Override + public boolean canDeal() { + return handsSum() <= THRESHOLD; + } +} diff --git a/src/main/java/domain/participant/Hands.java b/src/main/java/domain/participant/Hands.java new file mode 100644 index 0000000000..102d2966e8 --- /dev/null +++ b/src/main/java/domain/participant/Hands.java @@ -0,0 +1,88 @@ +package domain.participant; + +import domain.card.Card; +import domain.Result; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Hands { + + public static final int BLACK_JACK = 21; + private static final int EXTRA_ACE_VALUE = 10; + + private final List cards; + + public Hands(final List cards) { + this.cards = new ArrayList<>(cards); + } + + public static Hands createEmptyHands() { + return new Hands(new ArrayList<>()); + } + + public int sum() { + int total = cards.stream() + .mapToInt(Card::getCardNumber) + .sum(); + + return calculateTotalByAce(total); + } + + public void add(final Card card) { + cards.add(card); + } + + public boolean isBust() { + return sum() > BLACK_JACK; + } + + public boolean isBlackJack() { + return sum() == BLACK_JACK; + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + public int size() { + return cards.size(); + } + + public List getCards() { + return cards.stream() + .map(Card::toString) + .toList(); + } + + public Result calculateResult(final Hands target) { + return Result.calculateOf(this, target); + } + + private int calculateTotalByAce(final int total) { + if (hasAce() && total + EXTRA_ACE_VALUE <= BLACK_JACK) { + return total + EXTRA_ACE_VALUE; + } + + return total; + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + + if (!(target instanceof Hands hands)) { + return false; + } + + return Objects.equals(cards, hands.cards); + } + + @Override + public int hashCode() { + return Objects.hash(cards); + } +} diff --git a/src/main/java/domain/participant/Name.java b/src/main/java/domain/participant/Name.java new file mode 100644 index 0000000000..ffb95b64db --- /dev/null +++ b/src/main/java/domain/participant/Name.java @@ -0,0 +1,52 @@ +package domain.participant; + +import constants.ErrorCode; +import exception.InvalidPlayerNameException; +import java.util.Objects; + +public class Name { + + private final String value; + + public Name(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String name) { + validateNull(name); + validateBlank(name); + } + + private void validateNull(final String name) { + if (name == null) { + throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE); + } + } + + private void validateBlank(final String name) { + if (name.isBlank()) { + throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE); + } + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + if (!(target instanceof Name name)) { + return false; + } + return Objects.equals(value, name.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 0000000000..0bfa97b988 --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,72 @@ +package domain.participant; + +import domain.Result; +import domain.card.Card; +import java.util.List; +import java.util.Objects; + +public abstract class Participant { + + private final Name name; + private final Hands hands; + + protected Participant(final Name name, final Hands hands) { + this.name = name; + this.hands = hands; + } + + public abstract boolean canDeal(); + + public void add(final Card card) { + hands.add(card); + } + + public boolean isBust() { + return hands.isBust(); + } + + //TODO 시작시에 블랙잭일 때 쓸거에용 + public boolean isBlackJack() { + return hands.isBlackJack(); + } + + public int handsSum() { + return hands.sum(); + } + + public int handsSize() { + return hands.size(); + } + + public Result calculateResult(final Participant participant) { + return hands.calculateResult(participant.getHands()); + } + + public List getCardNames() { + return hands.getCards(); + } + + public String getName() { + return name.getValue(); + } + + public Hands getHands() { + return hands; + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + if (!(target instanceof Participant participant)) { + return false; + } + return Objects.equals(name, participant.name) && Objects.equals(hands, participant.hands); + } + + @Override + public int hashCode() { + return Objects.hash(name, hands); + } +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java new file mode 100644 index 0000000000..4674d6e99e --- /dev/null +++ b/src/main/java/domain/participant/Player.java @@ -0,0 +1,25 @@ +package domain.participant; + +import static domain.participant.Dealer.DEALER_NAME; + +import constants.ErrorCode; +import exception.ReservedPlayerNameException; + +public class Player extends Participant { + + public Player(final Name name, final Hands hands) { + super(name, hands); + validate(name); + } + + private void validate(final Name name) { + if (DEALER_NAME.equals(name)) { + throw new ReservedPlayerNameException(ErrorCode.RESERVED_NAME); + } + } + + @Override + public boolean canDeal() { + return handsSum() <= Hands.BLACK_JACK; + } +} diff --git a/src/main/java/domain/participant/Players.java b/src/main/java/domain/participant/Players.java new file mode 100644 index 0000000000..b007d830b9 --- /dev/null +++ b/src/main/java/domain/participant/Players.java @@ -0,0 +1,73 @@ +package domain.participant; + +import constants.ErrorCode; +import domain.Result; +import exception.DuplicatePlayerNameException; +import exception.InvalidPlayersSizeException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class Players { + + private static final int MIN_SIZE = 2; + private static final int MAX_SIZE = 8; + + private final List names; + + public Players(final List names) { + this.names = names; + } + + public static Players from(final List names) { + validate(names); + return new Players(mapToPlayers(names)); + } + + public void forEach(Consumer action) { + names.forEach(action); + } + + public boolean isAllBust() { + return names.stream() + .allMatch(Player::isBust); + } + + public Map getPlayersResult(final Dealer dealer) { + final Map result = new LinkedHashMap<>(); + for (Player name : names) { + result.put(name, name.calculateResult(dealer)); + } + return result; + } + + private static List mapToPlayers(final List names) { + return names.stream() + .map(String::trim) + .map(name -> new Player(new Name(name), Hands.createEmptyHands())) + .toList(); + } + + private static void validate(final List names) { + validateSize(names); + validateDuplicate(names); + } + + private static void validateSize(final List names) { + if (names.size() < MIN_SIZE || MAX_SIZE < names.size()) { + throw new InvalidPlayersSizeException(ErrorCode.INVALID_SIZE); + } + } + + private static void validateDuplicate(final List names) { + if (names.size() != Set.copyOf(names).size()) { + throw new DuplicatePlayerNameException(ErrorCode.DUPLICATE_NAME); + } + } + + public List getPlayers() { + return names; + } +} diff --git a/src/main/java/dto/DealerHandsDto.java b/src/main/java/dto/DealerHandsDto.java new file mode 100644 index 0000000000..dae3867715 --- /dev/null +++ b/src/main/java/dto/DealerHandsDto.java @@ -0,0 +1,22 @@ +package dto; + +import domain.participant.Participant; +import java.util.List; + +public class DealerHandsDto { + + private final String displayedCard; + + private DealerHandsDto(final String displayedCard) { + this.displayedCard = displayedCard; + } + + public static DealerHandsDto from(final Participant dealer) { + List cards = dealer.getCardNames(); + return new DealerHandsDto(cards.get(0)); + } + + public String getDisplayedCard() { + return displayedCard; + } +} diff --git a/src/main/java/dto/ParticipantDto.java b/src/main/java/dto/ParticipantDto.java new file mode 100644 index 0000000000..795bae6b57 --- /dev/null +++ b/src/main/java/dto/ParticipantDto.java @@ -0,0 +1,11 @@ +package dto; + +import domain.participant.Participant; +import java.util.List; + +public record ParticipantDto(String name, List cards, int totalSum) { + + public static ParticipantDto from(final Participant player) { + return new ParticipantDto(player.getName(), player.getCardNames(), player.handsSum()); + } +} diff --git a/src/main/java/dto/ParticipantsDto.java b/src/main/java/dto/ParticipantsDto.java new file mode 100644 index 0000000000..c7a2984c4a --- /dev/null +++ b/src/main/java/dto/ParticipantsDto.java @@ -0,0 +1,42 @@ +package dto; + +import domain.participant.Participant; +import domain.participant.Players; +import java.util.ArrayList; +import java.util.List; + +public class ParticipantsDto { + + private final List players; + + private ParticipantsDto(final List players) { + this.players = players; + } + + public static ParticipantsDto of(final Participant dealer, final Players players) { + List result = new ArrayList<>(); + result.add(ParticipantDto.from(dealer)); + for (Participant player : players.getPlayers()) { + result.add(ParticipantDto.from(player)); + } + return new ParticipantsDto(result); + } + + public static ParticipantsDto of(final Players players) { + List result = new ArrayList<>(); + for (Participant player : players.getPlayers()) { + result.add(ParticipantDto.from(player)); + } + return new ParticipantsDto(result); + } + + public List getNames() { + return players.stream() + .map(ParticipantDto::name) + .toList(); + } + + public List getPlayers() { + return players; + } +} diff --git a/src/main/java/exception/CustomException.java b/src/main/java/exception/CustomException.java new file mode 100644 index 0000000000..fa189d08d1 --- /dev/null +++ b/src/main/java/exception/CustomException.java @@ -0,0 +1,16 @@ +package exception; + +import constants.ErrorCode; + +public class CustomException extends RuntimeException { + + private final ErrorCode errorCode; + + public CustomException(final ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/exception/DuplicatePlayerNameException.java b/src/main/java/exception/DuplicatePlayerNameException.java new file mode 100644 index 0000000000..8eb7bb2b5a --- /dev/null +++ b/src/main/java/exception/DuplicatePlayerNameException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class DuplicatePlayerNameException extends CustomException { + + public DuplicatePlayerNameException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/InvalidCommandException.java b/src/main/java/exception/InvalidCommandException.java new file mode 100644 index 0000000000..ea970cc741 --- /dev/null +++ b/src/main/java/exception/InvalidCommandException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidCommandException extends CustomException { + + public InvalidCommandException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java new file mode 100644 index 0000000000..1ade7a5b23 --- /dev/null +++ b/src/main/java/exception/InvalidInputException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidInputException extends CustomException { + + public InvalidInputException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/InvalidPlayerNameException.java b/src/main/java/exception/InvalidPlayerNameException.java new file mode 100644 index 0000000000..2bf217ea37 --- /dev/null +++ b/src/main/java/exception/InvalidPlayerNameException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidPlayerNameException extends CustomException{ + + public InvalidPlayerNameException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/InvalidPlayersSizeException.java b/src/main/java/exception/InvalidPlayersSizeException.java new file mode 100644 index 0000000000..7242843584 --- /dev/null +++ b/src/main/java/exception/InvalidPlayersSizeException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidPlayersSizeException extends CustomException{ + + public InvalidPlayersSizeException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/InvalidSeparatorException.java b/src/main/java/exception/InvalidSeparatorException.java new file mode 100644 index 0000000000..76538c156d --- /dev/null +++ b/src/main/java/exception/InvalidSeparatorException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidSeparatorException extends CustomException{ + + public InvalidSeparatorException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/MessageDoesNotExistException.java b/src/main/java/exception/MessageDoesNotExistException.java new file mode 100644 index 0000000000..74251064ca --- /dev/null +++ b/src/main/java/exception/MessageDoesNotExistException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class MessageDoesNotExistException extends CustomException{ + + public MessageDoesNotExistException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/NoMoreCardException.java b/src/main/java/exception/NoMoreCardException.java new file mode 100644 index 0000000000..25551e0a6b --- /dev/null +++ b/src/main/java/exception/NoMoreCardException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class NoMoreCardException extends CustomException{ + + public NoMoreCardException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/exception/ReservedPlayerNameException.java b/src/main/java/exception/ReservedPlayerNameException.java new file mode 100644 index 0000000000..836ba97541 --- /dev/null +++ b/src/main/java/exception/ReservedPlayerNameException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class ReservedPlayerNameException extends CustomException { + + public ReservedPlayerNameException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..c432099732 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,58 @@ +package view; + +import constants.ErrorCode; +import exception.InvalidInputException; +import exception.InvalidSeparatorException; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private static final String NAME_SEPARATOR = ","; + + private final Scanner scanner; + + public InputView(final Scanner scanner) { + this.scanner = scanner; + } + + public List readNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String rawNames = scanner.nextLine().trim(); + validateBlank(rawNames); + validateSeparators(rawNames); + List names = List.of(rawNames.split(NAME_SEPARATOR)); + System.out.println(); + return names; + } + + public String readAnswer(String name) { + System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)", name); + System.out.println(); + String rawAnswer = scanner.nextLine().trim(); + validateBlank(rawAnswer); + return rawAnswer; + } + + private void validateBlank(final String rawNames) { + if (rawNames == null || rawNames.isBlank()) { + throw new InvalidInputException(ErrorCode.INVALID_INPUT); + } + } + + private void validateSeparators(final String rawNames) { + if (isInvalidSeparator(rawNames)) { + throw new InvalidSeparatorException(ErrorCode.INVALID_SEPARATOR); + } + } + + private boolean isInvalidSeparator(final String rawNames) { + if (rawNames.startsWith(NAME_SEPARATOR)) { + return true; + } + if (rawNames.endsWith(NAME_SEPARATOR)) { + return true; + } + return rawNames.contains(NAME_SEPARATOR.repeat(2)); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000000..87c607de6b --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,94 @@ +package view; + +import static domain.participant.Dealer.INIT_HANDS_SIZE; +import static domain.participant.Dealer.THRESHOLD; + +import constants.ErrorCode; +import domain.Result; +import domain.participant.Player; +import dto.DealerHandsDto; +import dto.ParticipantDto; +import dto.ParticipantsDto; +import java.util.List; +import java.util.Map; +import view.message.ErrorCodeMessage; + +public class OutputView { + + private static final String LINE = System.lineSeparator(); + private static final String FORM = "%s카드: %s%n"; + private static final String TOTAL_SUM_FORM = "%s 카드: %s - 결과: %d%n"; + private static final String RESULT_FORM = "%s: %s%n"; + private static final String ERROR_FORM = "[ERROR] %s%n"; + + + public void printStartDeal(final DealerHandsDto dealerHandsDto, final ParticipantsDto participantsDto) { + final String dealerCard = dealerHandsDto.getDisplayedCard(); + + final List playerNames = participantsDto.getNames(); + System.out.printf("딜러와 %s 에게 %d장을 나누었습니다.%n", format(playerNames), INIT_HANDS_SIZE); + System.out.printf("딜러: %s%n", dealerCard); + + for (ParticipantDto participantDto : participantsDto.getPlayers()) { + System.out.printf(FORM, participantDto.name(), format(participantDto.cards())); + } + System.out.print(LINE); + } + + public void printHands(final ParticipantDto participantDto) { + System.out.printf(FORM, participantDto.name(), format(participantDto.cards())); + } + + public void printDealerTurnMessage() { + System.out.printf("딜러는 %d이하라 한장의 카드를 더 받았습니다.%n%n", THRESHOLD); + } + + public void printHandsResult(final ParticipantsDto participantsDto) { + for (ParticipantDto participantDto : participantsDto.getPlayers()) { + System.out.printf(TOTAL_SUM_FORM, participantDto.name(), format(participantDto.cards()), + participantDto.totalSum()); + } + System.out.print(LINE); + } + + public void printGameResult(final Map dealerResult, final Map playerResult) { + System.out.println("## 최종결과"); + System.out.printf(RESULT_FORM, "딜러", format(dealerResult)); + for (Map.Entry entry : playerResult.entrySet()) { + System.out.printf(RESULT_FORM, entry.getKey().getName(), entry.getValue().getValue()); + } + } + + private String format(final Map dealerResult) { + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry entry : dealerResult.entrySet()) { + stringBuilder.append(entry.getValue()).append(entry.getKey().getValue()).append(" "); + } + + return stringBuilder.toString(); + } + + private String format(final List playerNames) { + return String.join(", ", playerNames); + } + + public void printBust() { + System.out.printf("BUST%n"); + } + + public void printBlackJack() { + System.out.printf("BLACK JACK!!!%n"); + } + + public void printException(final ErrorCode errorCode) { + System.out.printf(ERROR_FORM, ErrorCodeMessage.from(errorCode).getMessage()); + } + + public void printDealEndMessage(final boolean isBust) { + if (isBust) { + printBust(); + return; + } + printBlackJack(); + } +} diff --git a/src/main/java/view/message/ErrorCodeMessage.java b/src/main/java/view/message/ErrorCodeMessage.java new file mode 100644 index 0000000000..507dc908f7 --- /dev/null +++ b/src/main/java/view/message/ErrorCodeMessage.java @@ -0,0 +1,49 @@ +package view.message; + +import constants.ErrorCode; +import exception.MessageDoesNotExistException; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum ErrorCodeMessage { + + NOT_EXIST_MESSAGE(ErrorCode.NOT_EXIST_MESSAGE, "해당 메시지가 없습니다."), + INVALID_SEPARATOR(ErrorCode.INVALID_SEPARATOR, "유효하지 않은 구분자입니다."), + INVALID_INPUT(ErrorCode.INVALID_INPUT, "유효하지 않은 입력입니다."), + INVALID_SIZE(ErrorCode.INVALID_SIZE, "유효하지 않은 참여자 수입니다."), + DUPLICATE_NAME(ErrorCode.DUPLICATE_NAME, "중복된 이름은 사용할 수 없습니다."), + RESERVED_NAME(ErrorCode.RESERVED_NAME, "이름은 딜러일 수 없습니다."), + BLANK_VALUE(ErrorCode.BLANK_VALUE, "이름은 공백일 수 없습니다."), + EMPTY_CARD(ErrorCode.EMPTY_CARD, "뽑을 수 있는 카드가 없습니다."), + INVALID_COMMAND(ErrorCode.INVALID_COMMAND, "y 또는 n을 입력해주세요"), + ; + + private static final Map SUIT_MESSAGE = Arrays.stream(values()) + .collect(Collectors.toMap(ErrorCodeMessage::getCode, Function.identity())); + + + private final ErrorCode errorCode; + private final String message; + + ErrorCodeMessage(final ErrorCode errorCode, final String message) { + this.errorCode = errorCode; + this.message = message; + } + + public static ErrorCodeMessage from(ErrorCode errorCode) { + if (SUIT_MESSAGE.containsKey(errorCode)) { + return SUIT_MESSAGE.get(errorCode); + } + throw new MessageDoesNotExistException(ErrorCode.NOT_EXIST_MESSAGE); + } + + private ErrorCode getCode() { + return errorCode; + } + + public String getMessage() { + return message; + } +} diff --git a/src/test/java/domain/AnswerTest.java b/src/test/java/domain/AnswerTest.java new file mode 100644 index 0000000000..ea61387a17 --- /dev/null +++ b/src/test/java/domain/AnswerTest.java @@ -0,0 +1,18 @@ +package domain; + +import exception.InvalidCommandException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AnswerTest { + + @ParameterizedTest + @DisplayName("y혹은 n이 아닐시 예외가 발생한다.") + @ValueSource(strings = {"Y", "nn", "aa"}) + void invalidAnswer(String value) { + Assertions.assertThatThrownBy(() -> Answer.from(value)) + .isInstanceOf(InvalidCommandException.class); + } +} diff --git a/src/test/java/domain/CardTest.java b/src/test/java/domain/CardTest.java new file mode 100644 index 0000000000..39c5667e1d --- /dev/null +++ b/src/test/java/domain/CardTest.java @@ -0,0 +1,20 @@ +package domain; + +import domain.card.Card; +import java.util.List; +import java.util.Set; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardTest { + + @Test + @DisplayName("중복없는 52장의 카드를 생성한다.") + void generate() { + List values = Card.values(); + + Assertions.assertThat(values).hasSize(52); + Assertions.assertThat(Set.copyOf(values)).hasSize(52); + } +} diff --git a/src/test/java/domain/DealerTest.java b/src/test/java/domain/DealerTest.java new file mode 100644 index 0000000000..fcec8af224 --- /dev/null +++ b/src/test/java/domain/DealerTest.java @@ -0,0 +1,105 @@ +package domain; + +import static domain.HandsTestFixture.sum10Size2; +import static domain.HandsTestFixture.sum18Size2; +import static domain.HandsTestFixture.sum20Size3; +import static domain.HandsTestFixture.sum21Size2; +import static domain.Result.LOSE; +import static domain.Result.TIE; +import static domain.Result.WIN; + +import domain.participant.Dealer; +import domain.participant.Hands; +import domain.participant.Name; +import domain.participant.Player; +import domain.participant.Players; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DealerTest { + + @Test + @DisplayName("참여자에게 카드 2장을 나눠준다.") + void dealCards() { + //given + final Players players = Players.from(List.of("레디", "제제")); + final Dealer dealer = new Dealer(); + + //when + dealer.initHands(players); + + //then + Assertions.assertThat(players.getPlayers()).allMatch(player -> player.handsSize() == 2); + } + + @Test + @DisplayName("참여자의 답변이 y라면 카드를 한장 추가한다.") + void addOneCard() { + //given + final Player hitPlayer = new Player(new Name("레디"), Hands.createEmptyHands()); + final Player stayPlayer = new Player(new Name("제제"), Hands.createEmptyHands()); + + final Players players = new Players(List.of(hitPlayer, stayPlayer)); + + final Dealer dealer = new Dealer(); + dealer.initHands(players); + + //when + dealer.deal(hitPlayer, Answer.HIT); + dealer.deal(stayPlayer, Answer.STAY); + + //then + Assertions.assertThat(hitPlayer.handsSize()).isEqualTo(3); + Assertions.assertThat(stayPlayer.handsSize()).isEqualTo(2); + } + + @Test + @DisplayName("딜러의 카드의 합이 17이상이 될때까지 카드를 추가한다.") + void dealerDeal() { + //given + final Dealer dealer = new Dealer(sum10Size2); + + //when + dealer.deal(); + + //then + Assertions.assertThat(dealer.countAddedHands()).isPositive(); + Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17); + } + + @DisplayName("딜러의 카드의 합이 17이상이라면 카드를 추가하지 않는다") + @Test + void dealerNoDeal() { + //given + final Dealer dealer = new Dealer(sum18Size2); + + //when + dealer.deal(); + + //then + Assertions.assertThat(dealer.countAddedHands()).isZero(); + Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17); + } + + @DisplayName("딜러의 승패무를 판단한다.") + @Test + void dealerResult() { + // given + Player loser1 = new Player(new Name("레디"), sum18Size2); + Player loser2 = new Player(new Name("피케이"), sum18Size2); + Player winner = new Player(new Name("제제"), sum21Size2); + Player tier = new Player(new Name("브라운"), sum20Size3); + + Players players = new Players(List.of(loser1, loser2, winner, tier)); + Dealer dealer = new Dealer(sum20Size3); + + // when + Map expected = Map.of(WIN, 2, LOSE, 1, TIE, 1); + + // then + Assertions.assertThat(dealer.getDealerResult(players)).isEqualTo(expected); + } +} diff --git a/src/test/java/domain/HandsTest.java b/src/test/java/domain/HandsTest.java new file mode 100644 index 0000000000..8c5bb396d8 --- /dev/null +++ b/src/test/java/domain/HandsTest.java @@ -0,0 +1,84 @@ +package domain; + +import static domain.HandsTestFixture.sum19Size3Ace1; +import static domain.HandsTestFixture.sum19Size4Ace11; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum20Size3Ace1; +import static domain.HandsTestFixture.sum21Size3Ace11; +import static domain.card.Rank.EIGHT; +import static domain.card.Shape.CLOVER; + +import domain.card.Card; +import domain.participant.Hands; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +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 HandsTest { + + @Test + @DisplayName("카드를 가지고 있는 객체를 생성한다.") + void createPacket() { + Assertions.assertThatCode(Hands::createEmptyHands) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("카드를 추가한다.") + void addCard() { + //given + final Hands hands = Hands.createEmptyHands(); + + //when + hands.add(new Card(EIGHT, CLOVER)); + + //then + Assertions.assertThat(hands.size()).isEqualTo(1); + } + + @DisplayName("카드의 합을 구한다.") + @Test + void sum() { + Assertions.assertThat(sum20Size2.sum()).isEqualTo(20); + } + + @DisplayName("에이스를 11로 계산한다.") + @ParameterizedTest + @MethodSource("sumAce11ParameterProvider") + void sumAce11(final Hands hands, final int expected) { + // given & when + final int result = hands.sum(); + + // then + Assertions.assertThat(result).isEqualTo(expected); + } + + @DisplayName("에이스를 1로 계산한다.") + @ParameterizedTest + @MethodSource("sumAce1ParameterProvider") + void sumAce1(final Hands hands, final int expected) { + // given & when + final int result = hands.sum(); + + // then + Assertions.assertThat(result).isEqualTo(expected); + } + + static Stream sumAce11ParameterProvider() { + return Stream.of( + Arguments.of(sum21Size3Ace11, 21), + Arguments.of(sum19Size4Ace11, 19) + ); + } + + static Stream sumAce1ParameterProvider() { + return Stream.of( + Arguments.of(sum19Size3Ace1, 19), + Arguments.of(sum20Size3Ace1, 20) + ); + } +} diff --git a/src/test/java/domain/HandsTestFixture.java b/src/test/java/domain/HandsTestFixture.java new file mode 100644 index 0000000000..bb1adb416d --- /dev/null +++ b/src/test/java/domain/HandsTestFixture.java @@ -0,0 +1,49 @@ +package domain; + +import static domain.card.Rank.ACE; +import static domain.card.Rank.EIGHT; +import static domain.card.Rank.FIVE; +import static domain.card.Rank.FOUR; +import static domain.card.Rank.JACK; +import static domain.card.Rank.KING; +import static domain.card.Rank.NINE; +import static domain.card.Rank.QUEEN; +import static domain.card.Rank.SEVEN; +import static domain.card.Rank.SIX; +import static domain.card.Rank.TEN; +import static domain.card.Rank.THREE; +import static domain.card.Rank.TWO; +import static domain.card.Shape.CLOVER; +import static domain.card.Shape.DIAMOND; +import static domain.card.Shape.HEART; +import static domain.card.Shape.SPADE; + +import domain.card.Card; +import domain.participant.Hands; +import java.util.List; + +class HandsTestFixture { + + static final Hands sum10Size2 = new Hands(List.of(new Card(FIVE, SPADE), new Card(FIVE, HEART))); + static final Hands sum17Size3One = new Hands( + List.of(new Card(SEVEN, SPADE), new Card(FOUR, SPADE), new Card(SIX, SPADE))); + static final Hands sum17Size3Two = new Hands( + List.of(new Card(TEN, SPADE), new Card(THREE, SPADE), new Card(FOUR, SPADE))); + static final Hands sum18Size2 = new Hands(List.of(new Card(EIGHT, CLOVER), new Card(TEN, DIAMOND))); + static final Hands sum19Size4Ace11 = new Hands( + List.of(new Card(ACE, DIAMOND), new Card(TWO, CLOVER), new Card(FOUR, CLOVER), new Card(TWO, CLOVER))); + static final Hands sum19Size3Ace1 = new Hands( + List.of(new Card(ACE, HEART), new Card(NINE, SPADE), new Card(NINE, CLOVER))); + static final Hands sum20Size2 = new Hands(List.of(new Card(NINE, SPADE), new Card(ACE, SPADE))); + static final Hands sum20Size3 = new Hands( + List.of(new Card(SEVEN, SPADE), new Card(TWO, SPADE), new Card(ACE, SPADE))); + static final Hands sum20Size3Ace1 = new Hands( + List.of(new Card(ACE, DIAMOND), new Card(EIGHT, CLOVER), new Card(FIVE, CLOVER), new Card(SIX, CLOVER))); + static final Hands sum21Size2 = new Hands(List.of(new Card(QUEEN, HEART), new Card(ACE, SPADE))); + static final Hands sum21Size3Ace11 = new Hands( + List.of(new Card(ACE, HEART), new Card(EIGHT, SPADE), new Card(TWO, CLOVER))); + static final Hands bustHands = new Hands( + List.of(new Card(EIGHT, DIAMOND), new Card(TWO, DIAMOND), new Card(TWO, DIAMOND), new Card(KING, CLOVER))); + static final Hands noBustHands = new Hands(List.of(new Card(JACK, HEART), new Card(TEN, SPADE))); + static final Hands blackJack = new Hands(List.of(new Card(JACK, HEART), new Card(ACE, SPADE))); +} diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java new file mode 100644 index 0000000000..401a38f783 --- /dev/null +++ b/src/test/java/domain/NameTest.java @@ -0,0 +1,28 @@ +package domain; + +import domain.participant.Name; +import exception.InvalidPlayerNameException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + + @DisplayName("공백을 입력하면 예외를 발생시킨다.") + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + void BlankInputThrowException(String value) { + Assertions.assertThatThrownBy(() -> new Name(value)) + .isInstanceOf(InvalidPlayerNameException.class); + } + + @DisplayName("null을 입력하면 예외를 발생시킨다.") + @ParameterizedTest + @NullSource + void nullInputThrowException(String value) { + Assertions.assertThatThrownBy(() -> new Name(value)) + .isInstanceOf(InvalidPlayerNameException.class); + } +} diff --git a/src/test/java/domain/PlayerTest.java b/src/test/java/domain/PlayerTest.java new file mode 100644 index 0000000000..850c1599ec --- /dev/null +++ b/src/test/java/domain/PlayerTest.java @@ -0,0 +1,28 @@ +package domain; + +import domain.participant.Hands; +import domain.participant.Name; +import domain.participant.Player; +import exception.ReservedPlayerNameException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PlayerTest { + @DisplayName("이름으로 참여자를 생성한다.") + @Test + void createPlayerWithName() { + Assertions.assertThatCode(() -> new Player(new Name("pobi"), Hands.createEmptyHands())) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("참여자 이름이 딜러이면 예외가 발생한다.") + void validateName() { + final Name name = new Name("딜러"); + final Hands hands = Hands.createEmptyHands(); + + Assertions.assertThatThrownBy(() -> new Player(name, hands)) + .isInstanceOf(ReservedPlayerNameException.class); + } +} diff --git a/src/test/java/domain/PlayersTest.java b/src/test/java/domain/PlayersTest.java new file mode 100644 index 0000000000..f038cb7a75 --- /dev/null +++ b/src/test/java/domain/PlayersTest.java @@ -0,0 +1,142 @@ +package domain; + +import static domain.HandsTestFixture.bustHands; +import static domain.HandsTestFixture.noBustHands; +import static domain.HandsTestFixture.sum18Size2; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum20Size3; +import static domain.HandsTestFixture.sum21Size2; +import static domain.Result.LOSE; +import static domain.Result.TIE; +import static domain.Result.WIN; + +import domain.participant.Dealer; +import domain.participant.Name; +import domain.participant.Player; +import domain.participant.Players; +import exception.DuplicatePlayerNameException; +import exception.InvalidPlayersSizeException; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +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 PlayersTest { + + @Test + @DisplayName("참여자 이름 중복시 예외가 발생한다.") + void duplicatePlayerName() { + //given + final List names = List.of("redy", "redy"); + + //when & then + Assertions.assertThatThrownBy(() -> Players.from(names)) + .isInstanceOf(DuplicatePlayerNameException.class); + } + + @DisplayName("총 참여자 수가 2이상 8이하이면 참여자를 생성한다.") + @ParameterizedTest + @MethodSource("validPlayersSizeParameterProvider") + void validPlayersSize(final List names) { + Assertions.assertThatCode(() -> Players.from(names)) + .doesNotThrowAnyException(); + } + + @DisplayName("총 참여자 수는 2이상 8이하가 아니면 예외가 발생한다.") + @ParameterizedTest + @MethodSource("invalidPlayersSizeParameterProvider") + void invalidPlayersSize(final List names) { + Assertions.assertThatThrownBy(() -> Players.from(names)) + .isInstanceOf(InvalidPlayersSizeException.class); + } + + @Test + @DisplayName("참가자 중 버스트 되지 않은 참가자가 있다면 isAllBust가 False를 반환한다.") + void isAllBustFalse() { + //given + final Player bustPlayer = new Player(new Name("레디"), bustHands); + final Player noBustPlayer = new Player(new Name("제제"), noBustHands); + final Players players = new Players(List.of(bustPlayer, noBustPlayer)); + + //when && then + Assertions.assertThat(players.isAllBust()).isFalse(); + } + + @Test + @DisplayName("모든 참가자가 버스트되면 isAllBust가 True를 반환한다.") + void isAllBustTrue() { + //given + Player player1 = new Player(new Name("레디"), bustHands); + Player player2 = new Player(new Name("제제"), bustHands); + Player player3 = new Player(new Name("수달"), bustHands); + Player player4 = new Player(new Name("피케이"), bustHands); + + Players players = new Players(List.of(player1, player2, player3, player4)); + + //when && then + Assertions.assertThat(players.isAllBust()).isTrue(); + } + + @Test + @DisplayName("참여자의 승패무를 판단한다.") + void playerResult() { + //given + Player loser = new Player(new Name("레디"), sum18Size2); + Player winner = new Player(new Name("제제"), sum21Size2); + Player tier = new Player(new Name("수달"), sum20Size3); + + Players players = new Players(List.of(loser, winner, tier)); + Dealer dealer = new Dealer(sum20Size3); + + //when & then + Map expected = Map.of(loser, LOSE, winner, WIN, tier, TIE); + Assertions.assertThat(players.getPlayersResult(dealer)).isEqualTo(expected); + } + + @Test + @DisplayName("딜러가 버스트일때 참여자가 버스트가 아니면 WIN") + void all() { + //given + Dealer bustDealer = new Dealer(bustHands); + Player winner1 = new Player(new Name("레디"), sum18Size2); + Player winner2 = new Player(new Name("브라운"), sum20Size2); + Player loser = new Player(new Name("제제"), bustHands); + + Players players = new Players(List.of(winner1, winner2, loser)); + + //when + Map expectedPlayerResult = Map.of(winner1, WIN, winner2, WIN, loser, LOSE); + Map expectedDealerResult = Map.of(WIN, 1, LOSE, 2); + + //then + Assertions.assertThat(players.getPlayersResult(bustDealer)).isEqualTo(expectedPlayerResult); + Assertions.assertThat(bustDealer.getDealerResult(players)).isEqualTo(expectedDealerResult); + } + + static Stream validPlayersSizeParameterProvider() { + return Stream.of( + Arguments.of( + List.of("pobi", "jason") + ), + Arguments.of( + List.of("1", "2", "3", "4", "5", "6", "7", "8") + ) + ); + } + + static Stream invalidPlayersSizeParameterProvider() { + return Stream.of( + Arguments.of( + List.of("pobi") + ), + Arguments.of( + List.of("1", "2", "3", "4", "5", "6", "7", "8", "9") + ) + ); + } +} diff --git a/src/test/java/domain/ResultTest.java b/src/test/java/domain/ResultTest.java new file mode 100644 index 0000000000..01ad8e26a1 --- /dev/null +++ b/src/test/java/domain/ResultTest.java @@ -0,0 +1,48 @@ +package domain; + +import static domain.HandsTestFixture.blackJack; +import static domain.HandsTestFixture.bustHands; +import static domain.HandsTestFixture.sum17Size3One; +import static domain.HandsTestFixture.sum17Size3Two; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum20Size3; +import static domain.HandsTestFixture.sum21Size2; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResultTest { + + @DisplayName("카드 합이 같고 카드 갯수가 같으면 무승부이다.") + @Test + void isTie() { + Assertions.assertThat(sum17Size3One.calculateResult(sum17Size3Two)).isEqualTo(Result.TIE); + } + + @DisplayName("카드 합이 같은데 카드 갯수가 더 적으면 승리이다.") + @Test + void isWinBySize() { + Assertions.assertThat(sum20Size2.calculateResult(sum20Size3)).isEqualTo(Result.WIN); + } + + @Test + @DisplayName("카드 합이 21이하이면서 21에 가까운 카드가 승리한다.") + void isWin() { + Assertions.assertThat(sum21Size2.calculateResult(sum20Size2)).isEqualTo(Result.WIN); + Assertions.assertThat(sum20Size2.calculateResult(sum21Size2)).isEqualTo(Result.LOSE); + } + + @Test + @DisplayName("카드 합이 21초과이면 패배한다.") + void isLoseWhenCardSumGreater21() { + Assertions.assertThat(bustHands.calculateResult(sum20Size2)).isEqualTo(Result.LOSE); + } + + @Test + @DisplayName("blackjack이 이긴다.") + void isWinBlackJack() { + Assertions.assertThat(blackJack.calculateResult(sum20Size2)).isEqualTo(Result.WIN); + Assertions.assertThat(sum20Size2.calculateResult(blackJack)).isEqualTo(Result.LOSE); + } +} diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java new file mode 100644 index 0000000000..a7bf0c60bd --- /dev/null +++ b/src/test/java/domain/card/CardDeckTest.java @@ -0,0 +1,39 @@ +package domain.card; + +import static domain.participant.Dealer.DECK_SIZE; + +import exception.NoMoreCardException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardDeckTest { + + @DisplayName("카드를 52 * 6 만큼 생성한다.") + @Test + void generate() { + // given + CardDeck cardDeck = CardDeck.generate(DECK_SIZE); + + // when && then + Assertions.assertThat(cardDeck.size()).isEqualTo(52 * 6); + } + + @Test + @DisplayName("카드가 없는데 카드를 뽑을 경우 예외가 발생한다.") + void pop() { + //given + CardDeck cardDeck = CardDeck.generate(1); + + //when + int cardSize = 52; + while (cardSize > 0) { + cardDeck.pop(); + cardSize--; + } + + //then + Assertions.assertThatThrownBy(cardDeck::pop) + .isInstanceOf(NoMoreCardException.class); + } +}