diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..d57fe415174 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,117 @@ +# 블랙잭 규칙 + +## 참여자 + +* [x] 다시한번 룰 숙지 필요 + +**승** + +* (딜러 카드 합 < 참여자 카드 합) && 참여자가 버스트가 아닌 경우 +* 딜러가 버스트인 경우 && 참여자가 버스트가 아닌 경우 + +**무** + +* (딜러 카드 합 == 참여자 카드 합) && 참여자가 버스트가 아닌 경우 + +**패** + +* 참여자가 버스트인 경우 +* (딜러 카드 합 > 참여자 카드 합) + +
+ +## 블랙잭 + +* 카드의 갯수가 2장이고 카드의 합이 21인 경우 블랙잭이다. + +
+ +# 기능 요구 사항 + +* [x] 참여자 이름 입력 받는다. + * [x] 양 끝 공백을 제거한다. + * [x] 참여자는 쉼표(,)로 구분한다. + * [x] null 이나 공백인 경우 예외가 발생한다. + * [x] 쉼표로 시작시 예외가 발생한다. ex) ,pobi,jason + * [x] 쉼표로 끝날시 예외가 발생한다. ex) pobi,jason, + * [x] 쉼표가 연속으로 올시 예외가 발생한다. ex) pobi,,jason + * [x] 참여자 이름이 중복시 예외가 발생한다. +* [x] 총 참여자의 수는 2이상 8이하여야 한다. + +
+ +* [x] 각 참여자의 배팅 금액을 입력받는다. + * [x] 정수가 아닐 시 예외가 발생한다. + * [x] 양의 정수가 아닐 시 예외가 발생한다. + * [x] 10으로 나누어 떨어지지 않으면 예외가 발생한다. + * 블랙잭시 1.5배를 해주기 위함 + * [x] 배팅 금액은 최대 100_000_000로 이를 초과시 예외가 발생한다. + * 건전한 플레이를 하기 위함 + +
+ +* [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] 수익을 출력시 세자리마다 쉼표를 찍는다. ex) 100,000 +* [x] 참가자의 최종 수익을 계산한다. +* [x] 참가자만 블랙잭인 경우 배팅 금액의 1.5배를 받는다. +* [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 00000000000..2d98d55bfcb --- /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 00000000000..666205e8576 --- /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, + INVALID_BET_AMOUNT, + 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 00000000000..6e680b54054 --- /dev/null +++ b/src/main/java/controller/BlackJackController.java @@ -0,0 +1,76 @@ +package controller; + +import domain.Game; +import domain.amount.Amount; +import domain.amount.BetAmount; +import domain.participant.Dealer; +import domain.participant.Name; +import domain.participant.Names; +import domain.participant.Player; +import domain.participant.Players; +import dto.ParticipantsDto; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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 Names names = inputController.getNames(); + final Dealer dealer = new Dealer(); + final Players players = registerPlayers(names); + + final Game game = new Game(dealer, players); + + initHands(game); + dealWithPlayers(game); + dealerTurn(game); + + printFinalResult(players, dealer); + } + + private void dealerTurn(final Game game) { + game.dealerTurn(outputView::printDealerTurnMessage); + } + + private Players registerPlayers(final Names names) { + final List playerList = new ArrayList<>(); + for (final Name name : names.getPlayerNames()) { + final BetAmount betAmount = inputController.getBetAmount(name.getValue()); + playerList.add(new Player(name, betAmount)); + } + return new Players(playerList); + } + + private void dealWithPlayers(final Game game) { + if (game.isAlreadyEnd()) { + outputView.printDealerBlackJack(); + return; + } + game.checkingPlayersBlackJack(outputView::printBlackJack); + game.dealWithPlayers( + inputController::getAnswer, + outputView::printHands, + outputView::printPlayerEndMessage); + } + + private void initHands(final Game game) { + game.initHands(outputView::printStartDeal); + } + + private void printFinalResult(final Players players, final Dealer dealer) { + outputView.printHandsResult(ParticipantsDto.of(dealer, players)); + final Map finalResult = players.calculateResult(dealer); + final Amount dealerAmount = dealer.calculateRevenue(finalResult); + outputView.printGameResult(finalResult, dealerAmount); + } +} diff --git a/src/main/java/controller/InputController.java b/src/main/java/controller/InputController.java new file mode 100644 index 00000000000..ca00c39625d --- /dev/null +++ b/src/main/java/controller/InputController.java @@ -0,0 +1,74 @@ +package controller; + +import domain.participant.Answer; +import domain.amount.BetAmount; +import domain.participant.Names; +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 Names getNames() { + Names names; + do { + names = readNames(); + } while (names == null); + return names; + } + + public Answer getAnswer(String name) { + Answer answer; + do { + answer = readAnswer(name); + } while (answer == null); + return answer; + } + + public BetAmount getBetAmount(String name) { + BetAmount betAmount; + do { + betAmount = readBetAmount(name); + } while (betAmount == null); + return betAmount; + } + + private Names readNames() { + try { + List rawNames = inputView.readNames(); + return Names.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; + } + } + + private BetAmount readBetAmount(final String name) { + try { + Long value = inputView.readBetAmount(name); + return new BetAmount(value); + } catch (CustomException exception) { + outputView.printException(exception.getErrorCode()); + return null; + } + } +} diff --git a/src/main/java/domain/Game.java b/src/main/java/domain/Game.java new file mode 100644 index 00000000000..ff12a57050e --- /dev/null +++ b/src/main/java/domain/Game.java @@ -0,0 +1,94 @@ +package domain; + +import domain.participant.Answer; +import domain.participant.Dealer; +import domain.participant.Participant; +import domain.participant.Player; +import domain.participant.Players; +import dto.DealerHandsDto; +import dto.ParticipantDto; +import dto.ParticipantsDto; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +public class Game { + + private final Dealer dealer; + private final Players players; + + public Game(final Dealer dealer, final Players players) { + this.dealer = dealer; + this.players = players; + } + + public void initHands(BiConsumer handsPrinter) { + dealer.initHands(players); + handsPrinter.accept(DealerHandsDto.from(dealer), ParticipantsDto.of(players)); + } + + public boolean isAlreadyEnd() { + return dealer.isBlackJack(); + } + + public void checkingPlayersBlackJack(Consumer blackJackPrinter) { + players.filter(Participant::isBlackJack) + .forEach(player -> blackJackPrinter.accept(player.getName())); + } + + public void dealWithPlayers(Function answerReader, + Consumer handsPrinter, + Consumer endMessagePrinter) { + players.forEach(player -> deal(player, answerReader, + handsPrinter, + endMessagePrinter + )); + } + + public void deal(final Player player, Function answerReader, + Consumer handsPrinter, + Consumer endMessagePrinter) { + if (player.isBlackJack()) { + return; + } + boolean handsChanged = false; + boolean turnEnded = false; + + while (!turnEnded) { + final Answer answer = answerReader.apply(player.getName()); + dealer.deal(player, answer); + printHandsIfRequired(player, handsChanged, answer, handsPrinter); + handsChanged = true; + turnEnded = isTurnEnded(player, answer, endMessagePrinter); + } + } + + public void dealerTurn(Runnable dealerTurnMessagePrinter) { + if (players.isAllBust()) { + return; + } + dealer.deal(); + for (int i = 0; i < dealer.countAddedHands(); i++) { + dealerTurnMessagePrinter.run(); + } + } + + private void printHandsIfRequired(final Player player, final boolean handsChanged, final Answer answer, + Consumer handsPrinter) { + if (shouldShowHands(handsChanged, answer)) { + handsPrinter.accept(ParticipantDto.from(player)); + } + } + + private boolean isTurnEnded(final Player player, final Answer answer, Consumer endMessagePrinter) { + if (player.canDeal()) { + return !answer.isHit(); + } + endMessagePrinter.accept(player.isBust()); + return true; + } + + private boolean shouldShowHands(final boolean handsChanged, final Answer answer) { + return answer.isHit() || !handsChanged; + } +} diff --git a/src/main/java/domain/GameResult.java b/src/main/java/domain/GameResult.java new file mode 100644 index 00000000000..926a3bcdd69 --- /dev/null +++ b/src/main/java/domain/GameResult.java @@ -0,0 +1,61 @@ +package domain; + +import domain.amount.Amount; +import domain.amount.BetAmount; +import domain.participant.Hands; +import java.util.Arrays; +import java.util.function.BiPredicate; +import java.util.function.Function; + +public enum GameResult { + + BLACK_JACK_WIN(GameResult::blackJackWinningCondition, BetAmount::blackJackWinAmount), + WIN(GameResult::winningCondition, BetAmount::winAmount), + TIE(GameResult::tieCondition, BetAmount::tieAmount), + LOSE(GameResult::loseCondition, BetAmount::loseAmount); + + private final BiPredicate condition; + private final Function calculate; + + GameResult(final BiPredicate condition, final Function calculate) { + this.condition = condition; + this.calculate = calculate; + } + + public GameResult reverse() { + if (GameResult.WIN.equals(this) || GameResult.BLACK_JACK_WIN.equals(this)) { + return LOSE; + } + if (GameResult.LOSE.equals(this)) { + return WIN; + } + return TIE; + } + + public static GameResult calculateOf(final Hands hands, final Hands target) { + return Arrays.stream(GameResult.values()) + .filter(result -> result.condition.test(hands, target)) + .findFirst() + .orElseThrow(); + } + + public Amount apply(BetAmount betAmount) { + return calculate.apply(betAmount); + } + + private static boolean blackJackWinningCondition(Hands hands, Hands target) { + return hands.isBlackJack() && !target.isBlackJack(); + } + + private static boolean winningCondition(final Hands hands, final Hands target) { + return (!hands.isBust() && target.isBust()) || (hands.sum() > target.sum() && !hands.isBust()); + } + + private static boolean tieCondition(final Hands hands, final Hands target) { + return hands.sum() == target.sum() && !hands.isBust(); + } + + private static boolean loseCondition(final Hands hands, final Hands target) { + return hands.isBust() || hands.sum() < target.sum() || !target.isBust(); + } +} diff --git a/src/main/java/domain/amount/Amount.java b/src/main/java/domain/amount/Amount.java new file mode 100644 index 00000000000..15d899564d0 --- /dev/null +++ b/src/main/java/domain/amount/Amount.java @@ -0,0 +1,37 @@ +package domain.amount; + +import java.util.Objects; + +public class Amount { + + private final long value; + + public Amount(final long value) { + this.value = value; + } + + public long getValue() { + return value; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Amount amount)) { + return false; + } + return Objects.equals(value, amount.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/src/main/java/domain/amount/BetAmount.java b/src/main/java/domain/amount/BetAmount.java new file mode 100644 index 00000000000..dd7d46a75ad --- /dev/null +++ b/src/main/java/domain/amount/BetAmount.java @@ -0,0 +1,74 @@ +package domain.amount; + +import constants.ErrorCode; +import exception.InvalidBetAmountException; +import exception.OutOfRangeBetAmount; +import java.util.Objects; + +public class BetAmount { + + private static final int UNIT = 10; + private static final int MIN_RANGE = 10; + private static final int MAX_RANGE = 100_000_000; + + private final long value; + + public BetAmount(final long value) { + validate(value); + this.value = value; + } + + public Amount loseAmount() { + return new Amount(-value); + } + + public Amount blackJackWinAmount() { + return new Amount(value + (value / 2)); + } + + public Amount winAmount() { + return new Amount(value); + } + + public Amount tieAmount() { + return new Amount(0); + } + + private void validate(final long value) { + validateRange(value); + validateUnit(value); + } + + private void validateUnit(final long value) { + if (value % UNIT != 0) { + throw new InvalidBetAmountException(ErrorCode.INVALID_BET_AMOUNT); + } + } + + private void validateRange(final long value) { + if (value < MIN_RANGE || MAX_RANGE < value) { + throw new OutOfRangeBetAmount(ErrorCode.INVALID_BET_AMOUNT); + } + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + if (!(target instanceof BetAmount betAmount)) { + return false; + } + return Objects.equals(value, betAmount.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 00000000000..f32cdd138f5 --- /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 00000000000..b23329a53cd --- /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; + } + + 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 00000000000..aa92ef34659 --- /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 00000000000..093d73cfa8c --- /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/Answer.java b/src/main/java/domain/participant/Answer.java new file mode 100644 index 00000000000..0c110c0f7af --- /dev/null +++ b/src/main/java/domain/participant/Answer.java @@ -0,0 +1,28 @@ +package domain.participant; + +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/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java new file mode 100644 index 00000000000..a31b99cc618 --- /dev/null +++ b/src/main/java/domain/participant/Dealer.java @@ -0,0 +1,88 @@ +package domain.participant; + +import domain.amount.Amount; +import domain.GameResult; +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<>(GameResult.class); + + for (GameResult value : players.getPlayersResult(this).values()) { + GameResult reversed = value.reverse(); + dealerResult.put(reversed, dealerResult.getOrDefault(reversed, 0) + 1); + } + return dealerResult; + } + + public Amount calculateRevenue(final Map finalResult) { + long dealerAmount = finalResult.values() + .stream() + .map(Amount::getValue) + .mapToLong(Long::longValue) + .sum(); + return new Amount(-dealerAmount); + } + + @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 00000000000..89f6c1eb97e --- /dev/null +++ b/src/main/java/domain/participant/Hands.java @@ -0,0 +1,94 @@ +package domain.participant; + +import domain.GameResult; +import domain.card.Card; +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 && size() == 2; + } + + 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 GameResult calculateResult(final Hands target) { + return GameResult.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); + } + + @Override + public String toString() { + return "Hands=" + + cards; + } +} diff --git a/src/main/java/domain/participant/Name.java b/src/main/java/domain/participant/Name.java new file mode 100644 index 00000000000..d8d0744bc3f --- /dev/null +++ b/src/main/java/domain/participant/Name.java @@ -0,0 +1,57 @@ +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); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/domain/participant/Names.java b/src/main/java/domain/participant/Names.java new file mode 100644 index 00000000000..a6f5a1b67fa --- /dev/null +++ b/src/main/java/domain/participant/Names.java @@ -0,0 +1,49 @@ +package domain.participant; + +import constants.ErrorCode; +import exception.DuplicatePlayerNameException; +import exception.InvalidPlayersSizeException; +import java.util.List; +import java.util.Set; + +public class Names { + + private static final int MIN_SIZE = 2; + private static final int MAX_SIZE = 8; + + private final List playerNames; + + private Names(final List playerNames) { + this.playerNames = playerNames; + } + + public static Names from(final List names) { + List result = names.stream() + .map(String::trim) + .map(Name::new) + .toList(); + validate(result); + return new Names(result); + } + + 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 getPlayerNames() { + return playerNames; + } +} diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 00000000000..09e1fff645b --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,79 @@ +package domain.participant; + +import domain.GameResult; +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(); + } + + public boolean isBlackJack() { + return hands.isBlackJack(); + } + + public int handsSum() { + return hands.sum(); + } + + public int handsSize() { + return hands.size(); + } + + public GameResult 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); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return "Participant{" + + "name=" + name + + ", hands=" + hands + + '}'; + } +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java new file mode 100644 index 00000000000..48fd3197f7c --- /dev/null +++ b/src/main/java/domain/participant/Player.java @@ -0,0 +1,65 @@ +package domain.participant; + +import static domain.participant.Dealer.DEALER_NAME; + +import constants.ErrorCode; +import domain.amount.BetAmount; +import exception.ReservedPlayerNameException; +import java.util.Objects; + +public class Player extends Participant { + + private final BetAmount betAmount; + + Player(final Name name, final Hands hands, final BetAmount betAmount) { + super(name, hands); + this.betAmount = betAmount; + } + + Player(final Name name, final Hands hands) { + super(name, hands); + validate(name); + this.betAmount = new BetAmount(10); + } + + public Player(final Name name, final BetAmount betAmount) { + super(name, Hands.createEmptyHands()); + this.betAmount = betAmount; + } + + private void validate(final Name name) { + if (DEALER_NAME.equals(name)) { + throw new ReservedPlayerNameException(ErrorCode.RESERVED_NAME); + } + } + + public BetAmount getBetAmount() { + return betAmount; + } + + @Override + public boolean canDeal() { + return handsSum() < Hands.BLACK_JACK; + } + + @Override + public boolean equals(final Object target) { + if (this == target) { + return true; + } + if (!(target instanceof Player player)) { + return false; + } + return Objects.equals(getName(), player.getName()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getName()); + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/src/main/java/domain/participant/Players.java b/src/main/java/domain/participant/Players.java new file mode 100644 index 00000000000..bce0e099be4 --- /dev/null +++ b/src/main/java/domain/participant/Players.java @@ -0,0 +1,54 @@ +package domain.participant; + +import domain.GameResult; +import domain.amount.Amount; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public class Players { + + private final List names; + + public Players(final List names) { + this.names = names; + } + + public void forEach(Consumer action) { + names.forEach(action); + } + + public Stream filter(Predicate predicate) { + return names.stream().filter(predicate); + } + + 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; + } + + public Map calculateResult(final Dealer dealer) { + final Map playerAmountMap = new LinkedHashMap<>(); + + for (Player player : names) { + GameResult gameResult = player.calculateResult(dealer); + playerAmountMap.put(player, gameResult.apply(player.getBetAmount())); + } + return playerAmountMap; + } + + 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 00000000000..dae38677154 --- /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 00000000000..795bae6b575 --- /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 00000000000..c7a2984c4ab --- /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 00000000000..fa189d08d1f --- /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 00000000000..8eb7bb2b5ac --- /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/InvalidBetAmountException.java b/src/main/java/exception/InvalidBetAmountException.java new file mode 100644 index 00000000000..38c2ab15431 --- /dev/null +++ b/src/main/java/exception/InvalidBetAmountException.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class InvalidBetAmountException extends CustomException { + + public InvalidBetAmountException(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 00000000000..ea970cc7419 --- /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 00000000000..1ade7a5b234 --- /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 00000000000..0d43def32d7 --- /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 00000000000..9fbc5e47a08 --- /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 00000000000..36336cabc62 --- /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 00000000000..d7669bd6dd5 --- /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 00000000000..000c233da86 --- /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/OutOfRangeBetAmount.java b/src/main/java/exception/OutOfRangeBetAmount.java new file mode 100644 index 00000000000..509a26fcc52 --- /dev/null +++ b/src/main/java/exception/OutOfRangeBetAmount.java @@ -0,0 +1,10 @@ +package exception; + +import constants.ErrorCode; + +public class OutOfRangeBetAmount extends CustomException { + + public OutOfRangeBetAmount(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 00000000000..836ba975412 --- /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 00000000000..ee7217fd8f2 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,79 @@ +package view; + +import constants.ErrorCode; +import exception.InvalidBetAmountException; +import exception.InvalidInputException; +import exception.InvalidSeparatorException; +import java.util.Arrays; +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(); + validateBlank(rawNames); + validateSeparators(rawNames); + List names = Arrays.stream(rawNames.split(NAME_SEPARATOR)) + .map(String::trim) + .toList(); + System.out.println(); + return names; + } + + public Long readBetAmount(String name) { + System.out.printf("%s의 배팅 금액은?%n", name); + String rawAmount = scanner.nextLine().trim(); + validateBlank(rawAmount); + validateLong(rawAmount); + System.out.println(); + return Long.parseLong(rawAmount); + } + + + public String readAnswer(String name) { + System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)%n", name); + 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)); + } + + private void validateLong(final String rawAmount) { + try { + Long.parseLong(rawAmount); + } catch (NumberFormatException exception) { + throw new InvalidBetAmountException(ErrorCode.INVALID_BET_AMOUNT); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..00fe1b9271b --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,91 @@ +package view; + +import static domain.participant.Dealer.INIT_HANDS_SIZE; +import static domain.participant.Dealer.THRESHOLD; + +import constants.ErrorCode; +import domain.amount.Amount; +import domain.participant.Player; +import dto.DealerHandsDto; +import dto.ParticipantDto; +import dto.ParticipantsDto; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +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: %,d%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 playerAmountMap, final Amount amount) { + System.out.println("## 최종 수익"); + System.out.printf("딜러: %,d%n", amount.getValue()); + + for (Entry entry : playerAmountMap.entrySet()) { + System.out.printf(RESULT_FORM, entry.getKey().getName(), entry.getValue().getValue()); + } + } + + private String format(final List playerNames) { + return String.join(", ", playerNames); + } + + public void printBust() { + System.out.printf("BUST%n"); + } + + public void printBlackJack(final String name) { + System.out.printf("%s 축하드립니다! BLACK JACK!!!%n", name); + } + + public void printException(final ErrorCode errorCode) { + System.out.printf(ERROR_FORM, ErrorCodeMessage.from(errorCode).getMessage()); + } + + public void printPlayerEndMessage(final boolean isBust) { + if (isBust) { + printBust(); + return; + } + System.out.println("카드의 합이 21이라 더 이상 카드를 받을 수 없습니다."); + } + + public void printDealerBlackJack() { + System.out.println("딜러가 블랙잭!!!"); + } +} diff --git a/src/main/java/view/message/ErrorCodeMessage.java b/src/main/java/view/message/ErrorCodeMessage.java new file mode 100644 index 00000000000..6e8a9d9457f --- /dev/null +++ b/src/main/java/view/message/ErrorCodeMessage.java @@ -0,0 +1,50 @@ +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, "이름은 공백일 수 없습니다."), + INVALID_BET_AMOUNT(ErrorCode.INVALID_BET_AMOUNT, "유효하지 않은 배팅 금액입니다."), + 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 00000000000..c4f61646564 --- /dev/null +++ b/src/test/java/domain/AnswerTest.java @@ -0,0 +1,19 @@ +package domain; + +import domain.participant.Answer; +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/BetAmountTest.java b/src/test/java/domain/BetAmountTest.java new file mode 100644 index 00000000000..476a1c9cbfc --- /dev/null +++ b/src/test/java/domain/BetAmountTest.java @@ -0,0 +1,36 @@ +package domain; + +import domain.amount.BetAmount; +import exception.InvalidBetAmountException; +import exception.OutOfRangeBetAmount; +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.ValueSource; + +class BetAmountTest { + + @ParameterizedTest + @DisplayName("베팅 금액이 0 이하일 시 예외가 발생한다.") + @ValueSource(longs = {0, -1, -100000}) + void invalidBetAmount(long value) { + Assertions.assertThatThrownBy(() -> new BetAmount(value)) + .isInstanceOf(OutOfRangeBetAmount.class); + } + + @ParameterizedTest + @DisplayName("배팅 금액이 10으로 나누어 떨어지지 않을 시 예외가 발생한다.") + @ValueSource(longs = {11, 53, 10001}) + void invalidUnitBetAmount(long value) { + Assertions.assertThatThrownBy(() -> new BetAmount(value)) + .isInstanceOf(InvalidBetAmountException.class); + } + + @Test + @DisplayName("배팅 금액이 1억을 초과하면 예외가 발생한다.") + void overBetAmount() { + Assertions.assertThatThrownBy(() -> new BetAmount(100_000_010)) + .isInstanceOf(OutOfRangeBetAmount.class); + } +} diff --git a/src/test/java/domain/CardTest.java b/src/test/java/domain/CardTest.java new file mode 100644 index 00000000000..bc362cfca08 --- /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() { + final List values = Card.values(); + + Assertions.assertThat(values).hasSize(52); + Assertions.assertThat(Set.copyOf(values)).hasSize(52); + } +} diff --git a/src/test/java/domain/GameResultTest.java b/src/test/java/domain/GameResultTest.java new file mode 100644 index 00000000000..a791abe776f --- /dev/null +++ b/src/test/java/domain/GameResultTest.java @@ -0,0 +1,43 @@ +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.sum21Size3; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GameResultTest { + + @DisplayName("카드 합이 같다면 무승부이다.") + @Test + void isTie() { + Assertions.assertThat(sum17Size3One.calculateResult(sum17Size3Two)).isEqualTo(GameResult.TIE); + Assertions.assertThat(sum20Size2.calculateResult(sum20Size3)).isEqualTo(GameResult.TIE); + } + + @Test + @DisplayName("카드 합이 21이하이면서 21에 가까운 카드가 승리한다.") + void isWin() { + Assertions.assertThat(sum21Size3.calculateResult(sum20Size2)).isEqualTo(GameResult.WIN); + Assertions.assertThat(sum20Size2.calculateResult(sum21Size3)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("카드 합이 21초과이면 패배한다.") + void isLoseWhenCardSumGreater21() { + Assertions.assertThat(bustHands.calculateResult(sum20Size2)).isEqualTo(GameResult.LOSE); + } + + @Test + @DisplayName("blackjack이 이긴다.") + void isWinBlackJack() { + Assertions.assertThat(blackJack.calculateResult(sum20Size2)).isEqualTo(GameResult.BLACK_JACK_WIN); + Assertions.assertThat(sum20Size2.calculateResult(blackJack)).isEqualTo(GameResult.LOSE); + } +} diff --git a/src/test/java/domain/HandsTest.java b/src/test/java/domain/HandsTest.java new file mode 100644 index 00000000000..8c5bb396d81 --- /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 00000000000..c9a8d148242 --- /dev/null +++ b/src/test/java/domain/HandsTestFixture.java @@ -0,0 +1,50 @@ +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; + +public class HandsTestFixture { + + public static final Hands sum10Size2 = new Hands(List.of(new Card(FIVE, SPADE), new Card(FIVE, HEART))); + public static final Hands sum17Size3One = new Hands( + List.of(new Card(SEVEN, SPADE), new Card(FOUR, SPADE), new Card(SIX, SPADE))); + public static final Hands sum17Size3Two = new Hands( + List.of(new Card(TEN, SPADE), new Card(THREE, SPADE), new Card(FOUR, SPADE))); + public static final Hands sum18Size2 = new Hands(List.of(new Card(EIGHT, CLOVER), new Card(TEN, DIAMOND))); + public static final Hands sum19Size4Ace11 = new Hands( + List.of(new Card(ACE, DIAMOND), new Card(TWO, CLOVER), new Card(FOUR, CLOVER), new Card(TWO, CLOVER))); + public static final Hands sum19Size3Ace1 = new Hands( + List.of(new Card(ACE, HEART), new Card(NINE, SPADE), new Card(NINE, CLOVER))); + public static final Hands sum20Size2 = new Hands(List.of(new Card(NINE, SPADE), new Card(ACE, SPADE))); + public static final Hands sum20Size3 = new Hands( + List.of(new Card(SEVEN, SPADE), new Card(TWO, SPADE), new Card(ACE, SPADE))); + public static final Hands sum20Size3Ace1 = new Hands( + List.of(new Card(ACE, DIAMOND), new Card(EIGHT, CLOVER), new Card(FIVE, CLOVER), new Card(SIX, CLOVER))); + public static final Hands sum21Size3 = new Hands( + List.of(new Card(QUEEN, HEART), new Card(SIX, SPADE), new Card(FIVE, CLOVER))); + public static final Hands sum21Size3Ace11 = new Hands( + List.of(new Card(ACE, HEART), new Card(EIGHT, SPADE), new Card(TWO, CLOVER))); + public static final Hands bustHands = new Hands( + List.of(new Card(EIGHT, DIAMOND), new Card(TWO, DIAMOND), new Card(TWO, DIAMOND), new Card(KING, CLOVER))); + public static final Hands noBustHands = new Hands(List.of(new Card(JACK, HEART), new Card(TEN, SPADE))); + public 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 00000000000..401a38f7832 --- /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/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java new file mode 100644 index 00000000000..f4ccfa4c8c6 --- /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 + final CardDeck cardDeck = CardDeck.generate(DECK_SIZE); + + // when && then + Assertions.assertThat(cardDeck.size()).isEqualTo(52 * 6); + } + + @Test + @DisplayName("카드가 없는데 카드를 뽑을 경우 예외가 발생한다.") + void pop() { + //given + final CardDeck cardDeck = CardDeck.generate(1); + + //when + int cardSize = 52; + while (cardSize > 0) { + cardDeck.pop(); + cardSize--; + } + + //then + Assertions.assertThatThrownBy(cardDeck::pop) + .isInstanceOf(NoMoreCardException.class); + } +} diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java new file mode 100644 index 00000000000..1eb573b5406 --- /dev/null +++ b/src/test/java/domain/participant/DealerTest.java @@ -0,0 +1,122 @@ +package domain.participant; + +import static domain.HandsTestFixture.blackJack; +import static domain.HandsTestFixture.sum10Size2; +import static domain.HandsTestFixture.sum18Size2; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum20Size3; +import static domain.HandsTestFixture.sum21Size3; +import static domain.GameResult.LOSE; +import static domain.GameResult.TIE; +import static domain.GameResult.WIN; +import static org.junit.jupiter.api.Assertions.assertAll; + +import domain.amount.BetAmount; +import domain.GameResult; +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 Player player1 = new Player(new Name("레디"), new BetAmount(100)); + final Player player2 = new Player(new Name("제제"), new BetAmount(1000)); + final Players players = new Players(List.of(player1, player2)); + 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 + final Player loser1 = new Player(new Name("레디"), sum18Size2); + final Player loser2 = new Player(new Name("피케이"), sum18Size2); + final Player winner = new Player(new Name("제제"), sum21Size3); + final Player tier = new Player(new Name("브라운"), sum20Size3); + + final Players players = new Players(List.of(loser1, loser2, winner, tier)); + final Dealer dealer = new Dealer(sum20Size3); + + // when + final Map expected = Map.of(WIN, 2, LOSE, 1, TIE, 1); + + // then + Assertions.assertThat(dealer.getDealerResult(players)).isEqualTo(expected); + } + + @Test + @DisplayName("처음 나눠준 카드 두장의 합이 21이라면 블랙잭이다.") + void checkingBlackJack() { + //given + final Dealer dealer = new Dealer(sum20Size2); + final Player blackJackPlayer = new Player(new Name("수달"), blackJack); + final Player noBlackJackPlayer = new Player(new Name("레디"), sum18Size2); + + //when && then + assertAll(() -> Assertions.assertThat(dealer.isBlackJack()).isFalse(), + () -> Assertions.assertThat(blackJackPlayer.isBlackJack()).isTrue(), + () -> Assertions.assertThat(noBlackJackPlayer.isBlackJack()).isFalse()); + + } +} diff --git a/src/test/java/domain/participant/NamesTest.java b/src/test/java/domain/participant/NamesTest.java new file mode 100644 index 00000000000..4c21707a2e5 --- /dev/null +++ b/src/test/java/domain/participant/NamesTest.java @@ -0,0 +1,80 @@ +package domain.participant; + +import exception.DuplicatePlayerNameException; +import exception.InvalidPlayersSizeException; +import java.util.List; +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 NamesTest { + + @Test + @DisplayName("참여자 이름 중복시 예외가 발생한다.") + void duplicatePlayerName() { + //given + final List names = List.of("redy", "redy"); + + //when & then + Assertions.assertThatThrownBy(() -> Names.from(names)) + .isInstanceOf(DuplicatePlayerNameException.class); + } + + @ParameterizedTest + @DisplayName("참여자 이름에 공백이 있는 경우 제거하여 중복을 판단한다.") + @MethodSource("duplicateBlankNameParameterProvider") + void duplicateBlankName(final List names) { + Assertions.assertThatThrownBy(() -> Names.from(names)) + .isInstanceOf(DuplicatePlayerNameException.class); + } + + @DisplayName("총 참여자 수가 2이상 8이하이면 참여자를 생성한다.") + @ParameterizedTest + @MethodSource("validPlayersSizeParameterProvider") + void validPlayersSize(final List names) { + Assertions.assertThatCode(() -> Names.from(names)) + .doesNotThrowAnyException(); + } + + @DisplayName("총 참여자 수는 2이상 8이하가 아니면 예외가 발생한다.") + @ParameterizedTest + @MethodSource("invalidPlayersSizeParameterProvider") + void invalidPlayersSize(final List names) { + Assertions.assertThatThrownBy(() -> Names.from(names)) + .isInstanceOf(InvalidPlayersSizeException.class); + } + + static Stream duplicateBlankNameParameterProvider() { + return Stream.of( + Arguments.of(List.of("a", "a ", "b")), + Arguments.of(List.of(" a ", " a", "b", "c")), + Arguments.of(List.of("a", " a")) + ); + } + + 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/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java new file mode 100644 index 00000000000..eaa5b170595 --- /dev/null +++ b/src/test/java/domain/participant/PlayerTest.java @@ -0,0 +1,67 @@ +package domain.participant; + +import static domain.HandsTestFixture.bustHands; +import static domain.HandsTestFixture.sum18Size2; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum21Size3Ace11; + +import exception.ReservedPlayerNameException; +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 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); + } + + @ParameterizedTest + @DisplayName("참여자가 21 이상이면 더이상 딜을 할 수가 없다.") + @MethodSource("canNotDealParameterProvider") + void canNotDeal(Hands hands) { + final Name name = new Name("레디"); + final Player player = new Player(name, hands); + Assertions.assertThat(player.canDeal()).isFalse(); + } + + @ParameterizedTest + @DisplayName("참여자가 21 이하라면 딜 할 수가 있다.") + @MethodSource("canDealParameterProvider") + void canDeal(Hands hands) { + final Name name = new Name("레디"); + final Player player = new Player(name, hands); + Assertions.assertThat(player.canDeal()).isTrue(); + } + + static Stream canNotDealParameterProvider() { + return Stream.of( + Arguments.of(sum21Size3Ace11), + Arguments.of(bustHands) + ); + } + + static Stream canDealParameterProvider() { + return Stream.of( + Arguments.of(sum18Size2), + Arguments.of(sum20Size2) + ); + } +} diff --git a/src/test/java/domain/participant/PlayersTest.java b/src/test/java/domain/participant/PlayersTest.java new file mode 100644 index 00000000000..9ae8ac9cf10 --- /dev/null +++ b/src/test/java/domain/participant/PlayersTest.java @@ -0,0 +1,160 @@ +package domain.participant; + +import static domain.HandsTestFixture.blackJack; +import static domain.HandsTestFixture.bustHands; +import static domain.HandsTestFixture.noBustHands; +import static domain.HandsTestFixture.sum10Size2; +import static domain.HandsTestFixture.sum17Size3One; +import static domain.HandsTestFixture.sum18Size2; +import static domain.HandsTestFixture.sum19Size3Ace1; +import static domain.HandsTestFixture.sum20Size2; +import static domain.HandsTestFixture.sum20Size3; +import static domain.HandsTestFixture.sum21Size3; +import static domain.GameResult.LOSE; +import static domain.GameResult.TIE; +import static domain.GameResult.WIN; + +import domain.amount.Amount; +import domain.amount.BetAmount; +import domain.GameResult; +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 PlayersTest { + + @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 + final Player player1 = new Player(new Name("레디"), bustHands); + final Player player2 = new Player(new Name("제제"), bustHands); + final Player player3 = new Player(new Name("수달"), bustHands); + final Player player4 = new Player(new Name("피케이"), bustHands); + + final Players players = new Players(List.of(player1, player2, player3, player4)); + + //when && then + Assertions.assertThat(players.isAllBust()).isTrue(); + } + + @Test + @DisplayName("참여자의 승패무를 판단한다.") + void playerResult() { + //given + final Player loser = new Player(new Name("레디"), sum18Size2); + final Player winner = new Player(new Name("제제"), sum21Size3); + final Player tier = new Player(new Name("수달"), sum20Size3); + + final Players players = new Players(List.of(loser, winner, tier)); + final Dealer dealer = new Dealer(sum20Size3); + + //when & then + final Map expected = Map.of(loser, LOSE, winner, WIN, tier, TIE); + Assertions.assertThat(players.getPlayersResult(dealer)).isEqualTo(expected); + } + + @Test + @DisplayName("딜러가 버스트일때 참여자가 버스트가 아니면 WIN") + void all() { + //given + final Dealer bustDealer = new Dealer(bustHands); + final Player winner1 = new Player(new Name("레디"), sum18Size2); + final Player winner2 = new Player(new Name("브라운"), sum20Size2); + final Player loser = new Player(new Name("제제"), bustHands); + + final Players players = new Players(List.of(winner1, winner2, loser)); + + //when + final Map expectedPlayerResult = Map.of(winner1, WIN, winner2, WIN, loser, LOSE); + final Map expectedDealerResult = Map.of(WIN, 1, LOSE, 2); + + //then + Assertions.assertThat(players.getPlayersResult(bustDealer)).isEqualTo(expectedPlayerResult); + Assertions.assertThat(bustDealer.getDealerResult(players)).isEqualTo(expectedDealerResult); + } + + + @Test + @DisplayName("모든 플레이어가 이긴 경우, 최종 수익을 계산한다. (블랙잭인 경우는 없다)") + void calculateTotalAmountWhenAllPlayersWin() { + //given + final Dealer dealer = new Dealer(sum17Size3One); + final Player winner1 = new Player(new Name("레디"), sum20Size3, new BetAmount(1_000)); + final Player winner2 = new Player(new Name("제제"), sum21Size3, new BetAmount(2_000)); + final Players players = new Players(List.of(winner1, winner2)); + final Map expected = Map.of(winner1, new Amount(1_000), winner2, new Amount(2_000)); + + //when + final Map result = players.calculateResult(dealer); + + //then + Assertions.assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("참여자가 블랙잭인 경우 1.5배의 수익을 얻는다.") + void blackJackAmount() { + final Dealer dealer = new Dealer(sum17Size3One); + final Player blackJackPlayer = new Player(new Name("수달"), blackJack, new BetAmount(10_000)); + final Player loser = new Player(new Name("레디"), sum10Size2, new BetAmount(2_000)); + final Players players = new Players(List.of(blackJackPlayer, loser)); + + final Map expected = Map.of(blackJackPlayer, new Amount(15_000), loser, new Amount(-2_000)); + + //when + final Map result = players.calculateResult(dealer); + + //then + Assertions.assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("참가자와 딜러 모두 블랙잭인 경우 배팅 금액을 돌려받는다.") + void playerAndDealerBlackJack() { + final Dealer dealer = new Dealer(blackJack); + final Player blackJackPlayer = new Player(new Name("수달"), blackJack, new BetAmount(10_000)); + final Player loser = new Player(new Name("레디"), sum18Size2, new BetAmount(2_000)); + final Players players = new Players(List.of(blackJackPlayer, loser)); + final Map expected = Map.of(blackJackPlayer, new Amount(0), loser, new Amount(-2_000)); + + //when + final Map result = players.calculateResult(dealer); + + //then + Assertions.assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("딜러의 최종 수익을 계산한다.") + void dealerAmount() { + //given + final Dealer dealer = new Dealer(sum19Size3Ace1); + final Player winner1 = new Player(new Name("레디"), sum20Size3, new BetAmount(10_000)); + final Player winner2 = new Player(new Name("제제"), sum21Size3, new BetAmount(30_000)); + final Player loser1 = new Player(new Name("브라운"), sum17Size3One, new BetAmount(500_000)); + final Players players = new Players(List.of(winner1, winner2, loser1)); + + //when + final Map playerAmountMap = players.calculateResult(dealer); + final Amount amount = dealer.calculateRevenue(playerAmountMap); + final Amount expected = new Amount(460_000); + + //then + Assertions.assertThat(amount).isEqualTo(expected); + } +}