diff --git a/README.md b/README.md index 556099c4de..41d732ff26 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,98 @@ ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) + +## 블랙잭 개요 + +- 블랙잭 게임은 딜러와 플레이어가 진행한다. +- 카드의 합이 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. +- 숫자 계산은 카드의 숫자를 기본으로 하되, + - Ace: 1 or 11 + - King, Queen, Jack: 10 +- 게임을 시작하면, 딜러와 플레이어는 두 장의 카드를 지급받는다. +- 플레이어는 카드 숫자의 합이 21 미만이라면 카드를 계속 뽑을 수 있다. +- 딜러는 카드 숫자의 합이 16 이하인 경우에만 1장의 카드를 추가로 받아야 한다. + - 딜러는 최대 3장의 카드만 받을 수 있다. +- 게임이 완료되면, 각 플레이어의 승패를 출력한다. + +## 블랙잭 규칙 + +- `승`, `무`, `패`는 플레이어에 결과를 나타낸다. +- 플레이어 점수가 21점 초과일 때 + - 딜러 점수 상관없이 `패` +- 플레이어 점수가 21점 이하일 때 + - 딜러 점수가 21점 초과일 떄 + - 플레이어 `승` + - 딜러 점수가 21점 이하일 때 + - 플레이어 점수가 높으면 `승` + - 플레이어 점수가 낮으면 `패` + - 플레어어 점수와 같으면 `무` + +## 도메인 다이어그램 + +```mermaid +graph TD + BlackJackController --> InputView + BlackJackController --> OutputView + BlackJackController --> Participants + BlackJackController --> Deck + InputView --> Parser + Participants --> Participant + Participant --> Player + Participant --> Dealer + Participant --> Cards + Player --> PlayerName + Dealer --> Result + Cards --> Card + Card --> Suit + Card --> Number +``` + +## 기능 구현 목록 + +### 참가자 + +- [x] 카드를 받는다. +- [x] 점수를 확인한다. +- [x] 카드 추가 여부를 결정한다. +- [x] 여러 명일 수 있다. + - [x] 딜러는 포함되어야 한다. + - [x] 중복되는 이름은 가질 수 없다. + - [x] 최대 6명이다. + +### 플레이어 + +- [x] 이름을 가진다. + - [x] 최소 1자, 최대 10자까지 가능하다. + - [x] 중간 공백은 허용한다. + - [x] `딜러`라는 이름은 가질 수 없다. + +### 카드 + +- [x] 문양을 가진다. +- [x] 숫자를 가진다. +- [x] 점수를 계산한다. +- [x] 에이스인지 확인한다. +- [x] 점수 최댓값 초과 여부를 확인한다. +- [x] 점수 최댓값 여부를 확인한다. +- [x] 카드를 추가한다. +- [x] 카드 개수를 확인한다. + +### 덱 + +- [x] 카드 목록을 가진다. +- [x] 카드를 뽑는다. + - [x] 카드가 없으면 뽑을 수 없다. + +### 입력 + +- [x] 플레이어의 이름을 입력한다. + - [x] 앞, 뒤 공백은 제거한다. +- [x] 카드를 받을 여부를 입력한다. + +### 출력 + +- [x] 딜러와 플레이어의 카드 현황을 출력한다. +- [x] 딜러 카드 추가 여부를 출력한다. +- [x] 최종 결과를 출력한다. +- [x] 최종 승패를 출력한다. diff --git a/src/main/java/blackjack/BlackJackApplication.java b/src/main/java/blackjack/BlackJackApplication.java new file mode 100644 index 0000000000..6edf89d185 --- /dev/null +++ b/src/main/java/blackjack/BlackJackApplication.java @@ -0,0 +1,16 @@ +package blackjack; + +import blackjack.controller.BlackJackController; +import blackjack.view.InputView; +import blackjack.view.OutputView; + +public class BlackJackApplication { + + public static void main(String[] args) { + final InputView inputView = new InputView(); + final OutputView outputView = new OutputView(); + + final BlackJackController blackJackController = new BlackJackController(inputView, outputView); + blackJackController.run(); + } +} diff --git a/src/main/java/blackjack/controller/BlackJackController.java b/src/main/java/blackjack/controller/BlackJackController.java new file mode 100644 index 0000000000..a9cbbea76a --- /dev/null +++ b/src/main/java/blackjack/controller/BlackJackController.java @@ -0,0 +1,142 @@ +package blackjack.controller; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Deck; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Participant; +import blackjack.domain.participant.Participants; +import blackjack.domain.participant.Player; +import blackjack.domain.participant.Result; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import blackjack.view.dto.CardsResponse; +import blackjack.view.dto.DealerResultResponse; +import blackjack.view.dto.DealerStateResponse; +import blackjack.view.dto.ParticipantResponse; +import blackjack.view.dto.PlayerResultResponse; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class BlackJackController { + + private static final int INITIAL_DRAW_COUNT = 2; + + private final InputView inputView; + private final OutputView outputView; + + public BlackJackController(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + final Participants participants = new Participants(new Dealer(), gatherPlayers()); + final Deck deck = Deck.createUsingTrump(1); + + dealCards(participants, deck); + + drawCard(participants.getPlayers(), deck); + drawCard(participants.getDealer(), deck); + + printResult(participants); + } + + private List gatherPlayers() { + final List playerNames = inputView.readPlayerNames(); + return playerNames.stream() + .map(Player::new) + .collect(Collectors.toList()); + } + + private void dealCards(final Participants participants, final Deck deck) { + participants.drawCard(deck, INITIAL_DRAW_COUNT); + + final ParticipantResponse dealerResponse = getHiddenDealerResponse(participants.getDealer()); + final List playerResponse = getParticipantResponses(participants.getPlayers()); + + outputView.printDealCards(dealerResponse, playerResponse, INITIAL_DRAW_COUNT); + } + + private ParticipantResponse getHiddenDealerResponse(final Dealer dealer) { + final List hiddenCards = dealer.getCards().subList(0, INITIAL_DRAW_COUNT - 1); + final CardsResponse cardsResponse = new CardsResponse(-1, getCardInfos(hiddenCards)); + return new ParticipantResponse(dealer.getName(), cardsResponse); + } + + private List getCardInfos(final List cards) { + return cards.stream() + .map(card -> card.getNumberName() + card.getSuitName()) + .collect(Collectors.toList()); + } + + private List getParticipantResponses(final List participants) { + return participants.stream() + .map(this::getParticipantResponse) + .collect(Collectors.toList()); + } + + private ParticipantResponse getParticipantResponse(final Participant participant) { + final CardsResponse cardsResponse = new CardsResponse( + participant.getScore(), getCardInfos(participant.getCards()) + ); + return new ParticipantResponse(participant.getName(), cardsResponse); + } + + private void drawCard(final List players, final Deck deck) { + for (final Player player : players) { + drawCard(player, deck); + } + } + + private void drawCard(final Player player, final Deck deck) { + while (player.isDrawable() && inputView.readMoreDraw(player.getName())) { + player.drawCard(deck.draw()); + outputView.printHandedCardsWithoutScore(getParticipantResponse(player)); + } + } + + private void drawCard(final Dealer dealer, final Deck deck) { + if (dealer.isDrawable()) { + dealer.drawCard(deck.draw()); + outputView.printDealerDrawn(new DealerStateResponse(true, dealer.getMaximumDrawableScore())); + } + } + + private void printResult(final Participants participants) { + final List participantResponses = getParticipantResponses(participants.getParticipants()); + outputView.printHandedCardsWithScore(participantResponses); + + final Dealer dealer = participants.getDealer(); + final List players = participants.getPlayers(); + + final List playerResult = getPlayerResults(dealer, players); + final DealerResultResponse dealerResult = getDealerResult(dealer, playerResult); + + outputView.printFinalResult(dealerResult, playerResult); + } + + private List getPlayerResults(final Dealer dealer, final List players) { + return players.stream() + .map(player -> new PlayerResultResponse(player.getName(), dealer.showResult(player.getScore()))) + .collect(Collectors.toList()); + } + + private DealerResultResponse getDealerResult(final Dealer dealer, final List playerResults) { + final Map dealerResult = initResult(); + for (final PlayerResultResponse playerResult : playerResults) { + final Result result = playerResult.getResult().reverse(); + dealerResult.put(result, dealerResult.get(result) + 1); + } + return new DealerResultResponse(dealer.getName(), dealerResult); + } + + private Map initResult() { + final Map initResult = new EnumMap<>(Result.class); + for (final Result result : Result.values()) { + initResult.put(result, 0); + } + return initResult; + } +} diff --git a/src/main/java/blackjack/domain/card/Card.java b/src/main/java/blackjack/domain/card/Card.java new file mode 100644 index 0000000000..0ffedbbf67 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,28 @@ +package blackjack.domain.card; + +public class Card { + + private final Number number; + private final Suit suit; + + public Card(final Number number, final Suit suit) { + this.number = number; + this.suit = suit; + } + + public boolean isAce() { + return number == Number.ACE; + } + + public int getScore() { + return number.getScore(); + } + + public String getNumberName() { + return number.getName(); + } + + public String getSuitName() { + return suit.getName(); + } +} diff --git a/src/main/java/blackjack/domain/card/Cards.java b/src/main/java/blackjack/domain/card/Cards.java new file mode 100644 index 0000000000..50e06b748f --- /dev/null +++ b/src/main/java/blackjack/domain/card/Cards.java @@ -0,0 +1,58 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Cards { + + private static final int ACE_BONUS = 10; + private static final int MAXIMUM_UPDATABLE_SCORE = 21; + + private final List cards; + + public Cards() { + this(new ArrayList<>()); + } + + public Cards(final List cards) { + this.cards = new ArrayList<>(cards); + } + + public void addCard(final Card card) { + cards.add(card); + } + + public int calculateTotalScore() { + final int score = getTotalScore(); + + if (isExistAce() && isScoreUpdatable(score)) { + return score + ACE_BONUS; + } + + return score; + } + + private int getTotalScore() { + return cards.stream() + .mapToInt(Card::getScore) + .sum(); + } + + private boolean isExistAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + private boolean isScoreUpdatable(final int score) { + return score + ACE_BONUS <= MAXIMUM_UPDATABLE_SCORE; + } + + public int count() { + return cards.size(); + } + + public List getCards() { + return Collections.unmodifiableList(cards); + } +} diff --git a/src/main/java/blackjack/domain/card/Deck.java b/src/main/java/blackjack/domain/card/Deck.java new file mode 100644 index 0000000000..0ebc84e0ea --- /dev/null +++ b/src/main/java/blackjack/domain/card/Deck.java @@ -0,0 +1,45 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Stack; +import java.util.stream.Collectors; + +public class Deck { + + private static final Stack TRUMP; + + static { + final Stack pack = new Stack<>(); + final List cards = Arrays.stream(Suit.values()) + .flatMap(suit -> Arrays.stream(Number.values()) + .map(number -> new Card(number, suit)) + ) + .collect(Collectors.toList()); + pack.addAll(cards); + TRUMP = pack; + } + + private final Stack cards; + + public Deck(final Stack cards) { + Collections.shuffle(cards); + this.cards = cards; + } + + public static Deck createUsingTrump(final int count) { + final Stack pack = new Stack<>(); + for (int i = 0; i < count; i++) { + pack.addAll(TRUMP); + } + return new Deck(pack); + } + + public Card draw() { + if (cards.empty()) { + throw new IllegalStateException("덱에 더 이상의 카드가 없습니다."); + } + return cards.pop(); + } +} diff --git a/src/main/java/blackjack/domain/card/Number.java b/src/main/java/blackjack/domain/card/Number.java new file mode 100644 index 0000000000..caf3b8eb21 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Number.java @@ -0,0 +1,34 @@ +package blackjack.domain.card; + +public enum Number { + + 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 score; + + Number(final String name, final int score) { + this.name = name; + this.score = score; + } + + public String getName() { + return name; + } + + public int getScore() { + return score; + } +} diff --git a/src/main/java/blackjack/domain/card/Suit.java b/src/main/java/blackjack/domain/card/Suit.java new file mode 100644 index 0000000000..20c01c81e6 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Suit.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +public enum Suit { + + SPADE("스페이드"), + DIAMOND("다이아몬드"), + HEART("하트"), + CLOVER("클로버"); + + private final String name; + + Suit(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/participant/Dealer.java b/src/main/java/blackjack/domain/participant/Dealer.java new file mode 100644 index 0000000000..053b428ad2 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,61 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Cards; + +public class Dealer extends Participant { + + private static final String NAME = "딜러"; + private static final int MAXIMUM_DRAWABLE_SCORE = 16; + private static final int MAXIMUM_DRAWABLE_CARD_COUNT = 2; + + private final String name = NAME; + + public Dealer() { + super(); + } + + public Dealer(final Cards cards) { + super(cards); + } + + @Override + public boolean isDrawable() { + return isDrawableCardCount() && isDrawableScore(); + } + + private boolean isDrawableCardCount() { + return cards.count() <= MAXIMUM_DRAWABLE_CARD_COUNT; + } + + private boolean isDrawableScore() { + return cards.calculateTotalScore() <= MAXIMUM_DRAWABLE_SCORE; + } + + @Override + public boolean isDealer() { + return true; + } + + @Override + public String getName() { + return name; + } + + public Result showResult(final int score) { + final int dealerScore = getScore(); + if (score > BLACK_JACK_SCORE) { + return Result.LOSE; + } + if (dealerScore > BLACK_JACK_SCORE || dealerScore < score) { + return Result.WIN; + } + if (dealerScore > score) { + return Result.LOSE; + } + return Result.DRAW; + } + + public int getMaximumDrawableScore() { + return MAXIMUM_DRAWABLE_SCORE; + } +} diff --git a/src/main/java/blackjack/domain/participant/Participant.java b/src/main/java/blackjack/domain/participant/Participant.java new file mode 100644 index 0000000000..a2f3711c48 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participant.java @@ -0,0 +1,41 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import java.util.List; + +public abstract class Participant { + + protected static final int BLACK_JACK_SCORE = 21; + + protected final Cards cards; + + protected Participant() { + this(new Cards()); + } + + protected Participant(final Cards cards) { + this.cards = cards; + } + + public void drawCard(final Card card) { + if (!isDrawable()) { + throw new IllegalStateException("더 이상 카드를 뽑을 수 없습니다."); + } + cards.addCard(card); + } + + public int getScore() { + return cards.calculateTotalScore(); + } + + public List getCards() { + return cards.getCards(); + } + + public abstract boolean isDrawable(); + + public abstract boolean isDealer(); + + public abstract String getName(); +} diff --git a/src/main/java/blackjack/domain/participant/Participants.java b/src/main/java/blackjack/domain/participant/Participants.java new file mode 100644 index 0000000000..c7164afc5d --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participants.java @@ -0,0 +1,69 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Deck; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class Participants { + + private static final int MAXIMUM_PLAYER_COUNT = 5; + + private final List participants; + + public Participants(final Dealer dealer, final List players) { + validate(players); + + final List recruits = new ArrayList<>(); + recruits.add(dealer); + recruits.addAll(players); + + this.participants = recruits; + } + + private void validate(final List players) { + validateCount(players); + validateDuplicate(players); + } + + private void validateCount(final List players) { + if (players.size() > MAXIMUM_PLAYER_COUNT) { + throw new IllegalArgumentException("플레이어는 " + MAXIMUM_PLAYER_COUNT + "명을 초과할 수 없습니다"); + } + } + + private void validateDuplicate(final List players) { + final HashSet uniquePlayers = new HashSet<>(players); + + if (players.size() != uniquePlayers.size()) { + throw new IllegalArgumentException("플레이어 이름은 중복될 수 없습니다."); + } + } + + public void drawCard(final Deck deck, final int count) { + for (int i = 0; i < count; i++) { + participants.forEach(participant -> participant.drawCard(deck.draw())); + } + } + + public Dealer getDealer() { + return participants.stream() + .filter(Participant::isDealer) + .map(Dealer.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalStateException("딜러는 존재해야 합니다.")); + } + + public List getPlayers() { + return participants.stream() + .filter(participant -> !participant.isDealer()) + .map(Player.class::cast) + .collect(Collectors.toList()); + } + + public List getParticipants() { + return Collections.unmodifiableList(participants); + } +} diff --git a/src/main/java/blackjack/domain/participant/Player.java b/src/main/java/blackjack/domain/participant/Player.java new file mode 100644 index 0000000000..5e85f47c8d --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,50 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Cards; +import java.util.Objects; + +public class Player extends Participant { + + private final PlayerName name; + + public Player(final String name) { + this(name, new Cards()); + } + + public Player(final String name, final Cards cards) { + super(cards); + this.name = new PlayerName(name); + } + + @Override + public boolean isDrawable() { + return cards.calculateTotalScore() < BLACK_JACK_SCORE; + } + + @Override + public boolean isDealer() { + return false; + } + + @Override + public String getName() { + return name.getValue(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Player player = (Player) o; + return Objects.equals(name, player.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/blackjack/domain/participant/PlayerName.java b/src/main/java/blackjack/domain/participant/PlayerName.java new file mode 100644 index 0000000000..21c86d55be --- /dev/null +++ b/src/main/java/blackjack/domain/participant/PlayerName.java @@ -0,0 +1,61 @@ +package blackjack.domain.participant; + +import java.util.Objects; + +public class PlayerName { + + private static final String RESTRICT = "딜러"; + private static final int MAXIMUM_LENGTH = 10; + + private final String value; + + public PlayerName(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + validateBlank(value); + validateLength(value); + validateRestrict(value); + } + + private void validateBlank(final String value) { + if (value.isBlank()) { + throw new IllegalArgumentException("이름은 공백일 수 없습니다. 현재 이름: " + value); + } + } + + private void validateLength(final String value) { + if (value.length() > MAXIMUM_LENGTH) { + throw new IllegalArgumentException("이름은 " + MAXIMUM_LENGTH + "글자 이하여야 합니다. 현재 이름: " + value); + } + } + + private void validateRestrict(final String value) { + if (Objects.equals(RESTRICT, value)) { + throw new IllegalArgumentException("이름은 " + RESTRICT + "일 수 없습니다. 현재 이름: " + value); + } + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlayerName playerName = (PlayerName) o; + return Objects.equals(value, playerName.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/blackjack/domain/participant/Result.java b/src/main/java/blackjack/domain/participant/Result.java new file mode 100644 index 0000000000..4cd3534388 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Result.java @@ -0,0 +1,28 @@ +package blackjack.domain.participant; + +public enum Result { + + WIN("승"), + DRAW("무"), + LOSE("패"); + + private final String name; + + Result(final String name) { + this.name = name; + } + + public Result reverse() { + if (this == WIN) { + return LOSE; + } + if (this == LOSE) { + return WIN; + } + return DRAW; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..a64789c41a --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,61 @@ +package blackjack.view; + +import static blackjack.view.InputView.MoreDraw.NO; +import static blackjack.view.InputView.MoreDraw.YES; +import static java.text.MessageFormat.format; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private final Scanner scanner = new Scanner(System.in); + + public List readPlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + + final String input = scanner.nextLine(); + final List names = Parser.split(input, ","); + + return Parser.trim(names); + } + + public boolean readMoreDraw(final String name) { + System.out.println(format("{0}은(는) 한장의 카드를 더 받겠습니까?(예는 {1}, 아니오는 {2})", name, YES, NO)); + + final String input = scanner.nextLine(); + + return MoreDraw.from(input).getState(); + } + + enum MoreDraw { + + YES("y", true), + NO("n", false); + + private final String symbol; + private final boolean state; + + MoreDraw(final String symbol, final boolean state) { + this.symbol = symbol; + this.state = state; + } + + public static MoreDraw from(final String symbol) { + return Arrays.stream(values()) + .filter(moreDraw -> moreDraw.symbol.equals(symbol)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 입력입니다.")); + } + + public boolean getState() { + return state; + } + + @Override + public String toString() { + return symbol; + } + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 0000000000..ffd81ae013 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,93 @@ +package blackjack.view; + +import static blackjack.domain.participant.Result.DRAW; +import static blackjack.domain.participant.Result.LOSE; +import static blackjack.domain.participant.Result.WIN; +import static java.text.MessageFormat.format; + +import blackjack.domain.participant.Result; +import blackjack.view.dto.CardsResponse; +import blackjack.view.dto.DealerResultResponse; +import blackjack.view.dto.DealerStateResponse; +import blackjack.view.dto.ParticipantResponse; +import blackjack.view.dto.PlayerResultResponse; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String RESULT_FORMAT = "{0}: {1}"; + + public void printDealCards( + final ParticipantResponse dealer, final List players, final int count + ) { + System.out.println(LINE_SEPARATOR + format("{0}와 {1}에게 {2}장을 나누었습니다.", + dealer.getName(), getPlayerNamesFormat(players), count)); + + printHandedCardsWithoutScore(dealer); + players.forEach(this::printHandedCardsWithoutScore); + + System.out.println(); + } + + private String getPlayerNamesFormat(final List players) { + return players.stream() + .map(ParticipantResponse::getName) + .collect(Collectors.joining(", ")); + } + + public void printHandedCardsWithoutScore(final ParticipantResponse participant) { + System.out.println(getHandedCardsWithoutScore(participant)); + } + + private String getHandedCardsWithoutScore(final ParticipantResponse participant) { + final CardsResponse cardsResponse = participant.getCardsResponse(); + final List cardInfos = cardsResponse.getCardInfos(); + + return format("{0}카드: {1}", participant.getName(), getCardInfosFormat(cardInfos)); + } + + private String getCardInfosFormat(final List cardInfos) { + return String.join(", ", cardInfos); + } + + public void printDealerDrawn(final DealerStateResponse dealerStateResponse) { + if (dealerStateResponse.isDrawn()) { + System.out.println( + LINE_SEPARATOR + format("딜러는 {0}이하라 한장의 카드를 더 받았습니다.", dealerStateResponse.getLimit()) + ); + } + } + + public void printHandedCardsWithScore(final List participantResponses) { + System.out.println(); + participantResponses.forEach(this::printHandedCardsWithScore); + } + + private void printHandedCardsWithScore(final ParticipantResponse participant) { + final CardsResponse cardsResponse = participant.getCardsResponse(); + final int totalScore = cardsResponse.getTotalScore(); + + System.out.println(format("{0} - {1}", getHandedCardsWithoutScore(participant), getScoreFormat(totalScore))); + } + + private String getScoreFormat(final int totalScore) { + return format("결과: {0}", totalScore); + } + + public void printFinalResult(final DealerResultResponse dealer, final List players) { + System.out.println(LINE_SEPARATOR + "## 최종 승패"); + System.out.println(format(RESULT_FORMAT, dealer.getName(), getDealerResult(dealer.getResult()))); + players.forEach(this::printPlayerResult); + } + + private String getDealerResult(final Map result) { + return format("{0}승 {1}무 {2}패", result.get(WIN), result.get(DRAW), result.get(LOSE)); + } + + private void printPlayerResult(final PlayerResultResponse player) { + System.out.println(format(RESULT_FORMAT, player.getName(), player.getResult().getName())); + } +} diff --git a/src/main/java/blackjack/view/Parser.java b/src/main/java/blackjack/view/Parser.java new file mode 100644 index 0000000000..95cbe01174 --- /dev/null +++ b/src/main/java/blackjack/view/Parser.java @@ -0,0 +1,18 @@ +package blackjack.view; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Parser { + + public static List split(final String value, final String delimiter) { + return Arrays.asList(value.split(delimiter, -1)); + } + + public static List trim(final List values) { + return values.stream() + .map(String::trim) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/blackjack/view/dto/CardsResponse.java b/src/main/java/blackjack/view/dto/CardsResponse.java new file mode 100644 index 0000000000..42fc131aad --- /dev/null +++ b/src/main/java/blackjack/view/dto/CardsResponse.java @@ -0,0 +1,22 @@ +package blackjack.view.dto; + +import java.util.List; + +public class CardsResponse { + + private final int totalScore; + private final List cardInfos; + + public CardsResponse(final int totalScore, final List cardInfos) { + this.totalScore = totalScore; + this.cardInfos = cardInfos; + } + + public int getTotalScore() { + return totalScore; + } + + public List getCardInfos() { + return cardInfos; + } +} diff --git a/src/main/java/blackjack/view/dto/DealerResultResponse.java b/src/main/java/blackjack/view/dto/DealerResultResponse.java new file mode 100644 index 0000000000..d7b8a6fb42 --- /dev/null +++ b/src/main/java/blackjack/view/dto/DealerResultResponse.java @@ -0,0 +1,23 @@ +package blackjack.view.dto; + +import blackjack.domain.participant.Result; +import java.util.Map; + +public class DealerResultResponse { + + private final String name; + private final Map result; + + public DealerResultResponse(final String name, final Map result) { + this.name = name; + this.result = result; + } + + public String getName() { + return name; + } + + public Map getResult() { + return result; + } +} diff --git a/src/main/java/blackjack/view/dto/DealerStateResponse.java b/src/main/java/blackjack/view/dto/DealerStateResponse.java new file mode 100644 index 0000000000..ffe733c4a2 --- /dev/null +++ b/src/main/java/blackjack/view/dto/DealerStateResponse.java @@ -0,0 +1,20 @@ +package blackjack.view.dto; + +public class DealerStateResponse { + + private final boolean isDrawn; + private final int limit; + + public DealerStateResponse(final boolean isDrawn, final int limit) { + this.isDrawn = isDrawn; + this.limit = limit; + } + + public boolean isDrawn() { + return isDrawn; + } + + public int getLimit() { + return limit; + } +} diff --git a/src/main/java/blackjack/view/dto/ParticipantResponse.java b/src/main/java/blackjack/view/dto/ParticipantResponse.java new file mode 100644 index 0000000000..47d21d60c0 --- /dev/null +++ b/src/main/java/blackjack/view/dto/ParticipantResponse.java @@ -0,0 +1,20 @@ +package blackjack.view.dto; + +public class ParticipantResponse { + + private final String name; + private final CardsResponse cardsResponse; + + public ParticipantResponse(final String name, final CardsResponse cardsResponse) { + this.name = name; + this.cardsResponse = cardsResponse; + } + + public String getName() { + return name; + } + + public CardsResponse getCardsResponse() { + return cardsResponse; + } +} diff --git a/src/main/java/blackjack/view/dto/PlayerResultResponse.java b/src/main/java/blackjack/view/dto/PlayerResultResponse.java new file mode 100644 index 0000000000..45187d53bd --- /dev/null +++ b/src/main/java/blackjack/view/dto/PlayerResultResponse.java @@ -0,0 +1,22 @@ +package blackjack.view.dto; + +import blackjack.domain.participant.Result; + +public class PlayerResultResponse { + + private final String name; + private final Result result; + + public PlayerResultResponse(final String name, final Result result) { + this.name = name; + this.result = result; + } + + public String getName() { + return name; + } + + public Result getResult() { + return result; + } +} diff --git a/src/test/java/blackjack/domain/card/CardTest.java b/src/test/java/blackjack/domain/card/CardTest.java new file mode 100644 index 0000000000..d1b46805ad --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,67 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CardTest { + + @Test + void 문양_이름을_확인한다() { + final Suit suit = Suit.SPADE; + final Number number = Number.ACE; + + final Card card = new Card(number, suit); + + assertThat(card.getSuitName()).isEqualTo("스페이드"); + } + + @Test + void 숫자_이름을_확인한다() { + final Suit suit = Suit.SPADE; + final Number number = Number.ACE; + + final Card card = new Card(number, suit); + + assertThat(card.getNumberName()).isEqualTo("A"); + } + + @Test + void 숫자_점수를_확인한다() { + final Suit suit = Suit.SPADE; + final Number number = Number.ACE; + + final Card card = new Card(number, suit); + + assertThat(card.getScore()).isEqualTo(1); + } + + @Nested + class isAce_메서드는 { + + @Test + void 카드가_에이스라면_true_반환한다() { + final Suit suit = Suit.SPADE; + final Number number = Number.ACE; + + final Card card = new Card(number, suit); + + assertThat(card.isAce()).isTrue(); + } + + @Test + void 카드가_에이스가_아니라면_false_반환한다() { + final Suit suit = Suit.SPADE; + final Number number = Number.TEN; + + final Card card = new Card(number, suit); + + assertThat(card.isAce()).isFalse(); + } + } +} diff --git a/src/test/java/blackjack/domain/card/CardsTest.java b/src/test/java/blackjack/domain/card/CardsTest.java new file mode 100644 index 0000000000..4cea664ffc --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardsTest.java @@ -0,0 +1,69 @@ +package blackjack.domain.card; + +import static blackjack.domain.card.Number.ACE; +import static blackjack.domain.card.Number.JACK; +import static blackjack.domain.card.Number.QUEEN; +import static blackjack.domain.card.Number.TEN; +import static blackjack.domain.card.Number.TWO; +import static blackjack.domain.card.Suit.CLOVER; +import static blackjack.domain.card.Suit.DIAMOND; +import static blackjack.domain.card.Suit.HEART; +import static blackjack.domain.card.Suit.SPADE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +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; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CardsTest { + + private static Stream generateCardsWithoutAce() { + return Stream.of( + Arguments.of(List.of(new Card(TWO, SPADE), new Card(JACK, HEART)), 12), + Arguments.of(List.of(), 0) + ); + } + + private static Stream generateCardsWithAce() { + return Stream.of( + Arguments.of(List.of(new Card(ACE, SPADE), new Card(JACK, HEART)), 21), + Arguments.of(List.of(new Card(ACE, SPADE), new Card(ACE, HEART), new Card(TEN, CLOVER)), 12), + Arguments.of(List.of(new Card(ACE, SPADE), new Card(QUEEN, HEART), new Card(TEN, CLOVER)), 21), + Arguments.of(List.of(new Card(ACE, SPADE), new Card(ACE, HEART)), 12) + ); + } + + @ParameterizedTest + @MethodSource("generateCardsWithoutAce") + void 점수를_계산한다(final List cardPack, final int totalScore) { + final Cards cards = new Cards(cardPack); + + assertThat(cards.calculateTotalScore()).isEqualTo(totalScore); + } + + @ParameterizedTest + @MethodSource("generateCardsWithAce") + void 에이스가_포함된_경우_최적의_점수를_계산한다(final List cardPack, final int totalScore) { + final Cards cards = new Cards(cardPack); + + assertThat(cards.calculateTotalScore()).isEqualTo(totalScore); + } + + @Test + void 카드를_추가한다() { + final Cards cards = new Cards(new ArrayList<>()); + + final Card card = new Card(ACE, DIAMOND); + cards.addCard(card); + + assertThat(cards.count()).isEqualTo(1); + } +} diff --git a/src/test/java/blackjack/domain/card/DeckTest.java b/src/test/java/blackjack/domain/card/DeckTest.java new file mode 100644 index 0000000000..cc77590554 --- /dev/null +++ b/src/test/java/blackjack/domain/card/DeckTest.java @@ -0,0 +1,42 @@ +package blackjack.domain.card; + +import static blackjack.domain.card.Number.ACE; +import static blackjack.domain.card.Suit.SPADE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Stack; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DeckTest { + + @Nested + class draw_메서드는 { + + @Test + void 카드가_남아있으면_카드를_뽑는다() { + final Stack cards = new Stack<>(); + final Card card = new Card(ACE, SPADE); + cards.push(card); + + final Deck deck = new Deck(cards); + + assertThat(deck.draw()).isEqualTo(card); + } + + @Test + void 카드가_남아있지_않으면_예외를_던진다() { + final Stack cards = new Stack<>(); + + final Deck deck = new Deck(cards); + + assertThatThrownBy(deck::draw) + .isInstanceOf(IllegalStateException.class); + } + } +} diff --git a/src/test/java/blackjack/domain/participant/DealerTest.java b/src/test/java/blackjack/domain/participant/DealerTest.java new file mode 100644 index 0000000000..eb5b93c83f --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,209 @@ +package blackjack.domain.participant; + +import static blackjack.domain.card.Number.ACE; +import static blackjack.domain.card.Number.FIVE; +import static blackjack.domain.card.Number.QUEEN; +import static blackjack.domain.card.Number.SEVEN; +import static blackjack.domain.card.Number.SIX; +import static blackjack.domain.card.Number.TWO; +import static blackjack.domain.card.Suit.CLOVER; +import static blackjack.domain.card.Suit.DIAMOND; +import static blackjack.domain.card.Suit.HEART; +import static blackjack.domain.participant.Result.DRAW; +import static blackjack.domain.participant.Result.LOSE; +import static blackjack.domain.participant.Result.WIN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DealerTest { + + @Nested + class isDrawable_메서드는 { + + @Test + void 카드가_2장_이하이고_점수가_16점_이하라면_true_반환한다() { + final Cards cards = new Cards(List.of( + new Card(QUEEN, CLOVER), + new Card(SIX, HEART) + )); //16점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.isDrawable()).isTrue(); + } + + @Test + void 카드가_2장_이하이고_점수가_16점_초과라면_false_반환한다() { + final Cards cards = new Cards(List.of( + new Card(QUEEN, CLOVER), + new Card(SEVEN, HEART) + )); //17점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.isDrawable()).isFalse(); + } + + @Test + void 카드가_2장_초과라면_false_반환한다() { + final Cards cards = new Cards(List.of( + new Card(TWO, CLOVER), + new Card(SIX, HEART), + new Card(SEVEN, DIAMOND) + )); // 15점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.isDrawable()).isFalse(); + } + } + + @Nested + class drawCard_메서드는 { + + @Test + void 카드를_받을_수_없는_상태라면_예외를_던진다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(ACE, HEART) + )); + final Cards cards = new Cards(cardPack); + final Dealer dealer = new Dealer(cards); + + assertThatThrownBy(() -> dealer.drawCard(new Card(TWO, DIAMOND))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 카드를_받을_수_있는_상태라면_카드를_받는다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(SIX, HEART) + )); + final Cards cards = new Cards(cardPack); + final Dealer dealer = new Dealer(cards); + + dealer.drawCard(new Card(TWO, DIAMOND)); + + assertThat(dealer.isDrawable()).isFalse(); + } + } + + @Test + void 카드를_받는다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(SIX, HEART) + )); + final Cards cards = new Cards(cardPack); + final Dealer dealer = new Dealer(cards); + + dealer.drawCard(new Card(ACE, DIAMOND)); + + assertThat(dealer.isDrawable()).isFalse(); + } + + @Test + void 점수를_확인한다() { + final Cards cards = new Cards(List.of( + new Card(TWO, CLOVER), + new Card(SIX, HEART), + new Card(SEVEN, DIAMOND) + )); + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.getScore()).isEqualTo(15); + } + + @Test + void 딜러는_딜러이다() { + final Dealer dealer = new Dealer(); + + assertThat(dealer.isDealer()).isTrue(); + } + + @Test + void 딜러의_이름은_딜러이다() { + final Dealer dealer = new Dealer(); + + assertThat(dealer.getName()).isEqualTo("딜러"); + } + + @Test + void 딜러가_카드를_뽑을_수_있는_최대_점수는_16점이다() { + final Dealer dealer = new Dealer(); + + assertThat(dealer.getMaximumDrawableScore()).isEqualTo(16); + } + + @Nested + class showResult_메서드는 { + + @Nested + class 플레이어_점수가_블랙잭_점수를_초과하면 { + + @Test + void LOSE_반환한다() { + final Dealer dealer = new Dealer(); + + assertThat(dealer.showResult(22)).isEqualTo(LOSE); + } + } + + @Nested + class 딜러_점수가_블랙잭_점수를_초과하면 { + + @Test + void 플레이어_점수가_블랙잭_점수_이하라면_WIN_반환한다() { + final Dealer dealer = new Dealer(); + + assertThat(dealer.showResult(21)).isEqualTo(WIN); + } + } + + @Nested + class 딜러와_플레이어_점수_모두_블랙잭_점수_이하라면 { + + @Test + void 딜러_점수가_플레이어_점수보다_낮으면_WIN_반환한다() { + final Cards cards = new Cards(List.of( + new Card(ACE, HEART), + new Card(FIVE, DIAMOND) + )); //16점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.showResult(17)).isEqualTo(WIN); + } + + @Test + void 딜러_점수가_플레이어_점수보다_높으면_LOSE_반환한다() { + final Cards cards = new Cards(List.of( + new Card(ACE, HEART), + new Card(FIVE, DIAMOND) + )); //16점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.showResult(15)).isEqualTo(LOSE); + } + + @Test + void 점수_같으면_DRAW_반환한다() { + final Cards cards = new Cards(List.of( + new Card(ACE, HEART), + new Card(FIVE, DIAMOND) + )); //16점 + final Dealer dealer = new Dealer(cards); + + assertThat(dealer.showResult(16)).isEqualTo(DRAW); + } + } + } +} diff --git a/src/test/java/blackjack/domain/participant/ParticipantsTest.java b/src/test/java/blackjack/domain/participant/ParticipantsTest.java new file mode 100644 index 0000000000..1d537493f4 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/ParticipantsTest.java @@ -0,0 +1,61 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ParticipantsTest { + + @Test + void 중복된_플레이어_이름이_존재하면_예외를_던진다() { + final Dealer dealer = new Dealer(); + final List players = List.of(new Player("toney"), new Player("toney")); + + assertThatThrownBy(() -> new Participants(dealer, players)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 플레이어가_5명_초과라면_예외를_던진다() { + final Dealer dealer = new Dealer(); + final List players = List.of( + new Player("dazzle"), + new Player("kokodak"), + new Player("toney"), + new Player("pobi"), + new Player("crong"), + new Player("jason") + ); + + assertThatThrownBy(() -> new Participants(dealer, players)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 딜러를_확인한다() { + final Dealer dealer = new Dealer(); + final List players = List.of(new Player("toney"), new Player("dazzle")); + + final Participants participants = new Participants(dealer, players); + + assertThat(participants.getDealer()).isEqualTo(dealer); + } + + @Test + void 플레이어를_확인한다() { + final Dealer dealer = new Dealer(); + final Player firstPlayer = new Player("toney"); + final Player secondPlayer = new Player("dazzle"); + final List players = List.of(firstPlayer, secondPlayer); + + final Participants participants = new Participants(dealer, players); + + assertThat(participants.getPlayers()).containsExactly(firstPlayer, secondPlayer); + } +} diff --git a/src/test/java/blackjack/domain/participant/PlayerNameTest.java b/src/test/java/blackjack/domain/participant/PlayerNameTest.java new file mode 100644 index 0000000000..bf09773193 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerNameTest.java @@ -0,0 +1,37 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PlayerNameTest { + + @Test + void 이름이_공백이면_예외를_던진다() { + final String input = " "; + + assertThatThrownBy(() -> new PlayerName(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"", "abcdeabcdea"}) + void 이름은_1자_이상_10자_이하가_아니라면_예외를_던진다(final String input) { + assertThatThrownBy(() -> new PlayerName(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 이름이_딜러라면_예외를_던진다() { + final String input = "딜러"; + + assertThatThrownBy(() -> new PlayerName(input)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/blackjack/domain/participant/PlayerTest.java b/src/test/java/blackjack/domain/participant/PlayerTest.java new file mode 100644 index 0000000000..40c51d6465 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,112 @@ +package blackjack.domain.participant; + +import static blackjack.domain.card.Number.ACE; +import static blackjack.domain.card.Number.KING; +import static blackjack.domain.card.Number.QUEEN; +import static blackjack.domain.card.Number.TWO; +import static blackjack.domain.card.Suit.CLOVER; +import static blackjack.domain.card.Suit.DIAMOND; +import static blackjack.domain.card.Suit.HEART; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PlayerTest { + + @Test + void 이름을_확인한다() { + final Cards cards = new Cards(List.of( + new Card(QUEEN, CLOVER), + new Card(QUEEN, HEART) + )); + final Player player = new Player("dazzle", cards); + + assertThat(player.getName()).isEqualTo("dazzle"); + } + + @Nested + class isDrawable_메소드는 { + + @Test + void 점수가_21미만이면_true_반환한다() { + final Cards cards = new Cards(List.of( + new Card(QUEEN, CLOVER), + new Card(QUEEN, HEART) + )); //20점 + final Player player = new Player("kokodak", cards); + + assertThat(player.isDrawable()).isTrue(); + } + + @Test + void 점수가_21이상이면_false_반환한다() { + final Cards cards = new Cards(List.of( + new Card(QUEEN, CLOVER), + new Card(ACE, HEART) + )); //21점 + final Player player = new Player("kokodak", cards); + + assertThat(player.isDrawable()).isFalse(); + } + } + + @Nested + class drawCard_메서드는 { + + @Test + void 카드를_받을_수_없는_상태라면_예외를_던진다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(ACE, HEART) + )); + final Cards cards = new Cards(cardPack); + final Player player = new Player("dazzle", cards); + + assertThatThrownBy(() -> player.drawCard(new Card(TWO, DIAMOND))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 카드를_받을_수_있는_상태라면_카드를_받는다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(KING, HEART) + )); + final Cards cards = new Cards(cardPack); + final Player player = new Player("dazzle", cards); + + player.drawCard(new Card(TWO, DIAMOND)); + + assertThat(player.isDrawable()).isFalse(); + } + } + + @Test + void 점수를_확인한다() { + final List cardPack = new ArrayList<>(List.of( + new Card(QUEEN, CLOVER), + new Card(KING, HEART) + )); + final Cards cards = new Cards(cardPack); + final Player player = new Player("dazzle", cards); + + assertThat(player.getScore()).isEqualTo(20); + } + + @Test + void 플레이어는_딜러가_아니다() { + final Player player = new Player("toney"); + + assertThat(player.isDealer()).isFalse(); + } +} diff --git a/src/test/java/blackjack/domain/participant/ResultTest.java b/src/test/java/blackjack/domain/participant/ResultTest.java new file mode 100644 index 0000000000..e431e25873 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/ResultTest.java @@ -0,0 +1,37 @@ +package blackjack.domain.participant; + +import static blackjack.domain.participant.Result.DRAW; +import static blackjack.domain.participant.Result.LOSE; +import static blackjack.domain.participant.Result.WIN; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class ResultTest { + + @Nested + class reverse_메서드는 { + + @Test + void WIN_이라면_LOSE_반환한다() { + final Result result = WIN; + + assertThat(result.reverse()).isEqualTo(LOSE); + } + + @Test + void LOSE_라면_WIN_반환한다() { + final Result result = LOSE; + + assertThat(result.reverse()).isEqualTo(WIN); + } + + @Test + void DRAW_라면_DRAW_반환한다() { + final Result result = DRAW; + + assertThat(result.reverse()).isEqualTo(DRAW); + } + } +} diff --git a/src/test/java/blackjack/view/ParserTest.java b/src/test/java/blackjack/view/ParserTest.java new file mode 100644 index 0000000000..8fa4220973 --- /dev/null +++ b/src/test/java/blackjack/view/ParserTest.java @@ -0,0 +1,40 @@ +package blackjack.view; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ParserTest { + + @Test + void 구분자를_기준으로_문자열을_파싱한다() { + final String value = "pobi,jason"; + + final List result = Parser.split(value, ","); + + assertThat(result).containsExactly("pobi", "jason"); + } + + @Test + void 구분자를_기준으로_빈문자열을_파싱한다() { + final String value = ",,"; + + final List result = Parser.split(value, ","); + + assertThat(result).containsExactly("", "", ""); + } + + @Test + void 문자열의_앞뒤_공백은_제거한다() { + final List value = List.of(" pobi", "jason ", " crong "); + + final List result = Parser.trim(value); + + assertThat(result).containsExactly("pobi", "jason", "crong"); + } +} diff --git a/src/test/java/techcourse/jcf/mission/GenericStudy.java b/src/test/java/techcourse/jcf/mission/GenericStudy.java new file mode 100644 index 0000000000..f1412059db --- /dev/null +++ b/src/test/java/techcourse/jcf/mission/GenericStudy.java @@ -0,0 +1,99 @@ +package techcourse.jcf.mission; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class GenericStudy { + + @Test + void 정수_타입_리스트를_생성한다() { + SimpleList values = new SimpleArrayList(); + values.add(1); + values.add(2); + + Integer first = values.get(0); + Integer second = values.get(1); + + assertAll( + () -> assertThat(first).isEqualTo(1), + () -> assertThat(second).isEqualTo(2) + ); + } + + @Test + void 배열을_받아서_리스트로_변환한다() { + String[] arrays = {"first", "second"}; + + SimpleList values = SimpleList.fromArrayToList(arrays); + + String first = values.get(0); + String second = values.get(1); + + assertAll( + () -> assertThat(first).isEqualTo("first"), + () -> assertThat(second).isEqualTo("second") + ); + } + + @Test + void 숫자타입이라면_모든_값을_더한다() { + Double[] doubleArrays = {0.5, 0.7}; + Integer[] intArrays = {1, 2}; + + SimpleList doubleList = SimpleList.fromArrayToList(doubleArrays); + SimpleList intList = SimpleList.fromArrayToList(intArrays); + + double doubleSum = SimpleList.sum(doubleList); + double intSum = SimpleList.sum(intList); + + assertAll( + () -> assertThat(doubleSum).isEqualTo(1.2), + () -> assertThat(intSum).isEqualTo(3) + ); + } + + @Test + void 음수를_필터링한다() { + Double[] doubleArrays = {-0.1, 0.5, 0.7}; + Integer[] intArrays = {-10, 1, 2}; + + SimpleList doubleList = SimpleList.fromArrayToList(doubleArrays); + SimpleList intList = SimpleList.fromArrayToList(intArrays); + + SimpleList filteredDoubleList = SimpleList.filterNegative(doubleList); + SimpleList filteredIntList = SimpleList.filterNegative(intList); + + assertAll( + () -> assertThat(filteredDoubleList.size()).isEqualTo(2), + () -> assertThat(filteredDoubleList.get(0)).isEqualTo(0.5), + () -> assertThat(filteredDoubleList.get(1)).isEqualTo(0.7), + () -> assertThat(filteredIntList.size()).isEqualTo(2), + () -> assertThat(filteredIntList.get(0)).isEqualTo(1), + () -> assertThat(filteredIntList.get(1)).isEqualTo(2) + ); + } + + @Test + void 리스트를_다른_리스트로_복사한다() { + class Printer { + } + class LaserPrinter extends Printer { + } + + LaserPrinter laserPrinter = new LaserPrinter(); + + SimpleList printers = new SimpleArrayList<>(); + SimpleList laserPrinters = new SimpleArrayList<>(); + laserPrinters.add(laserPrinter); + + SimpleList.copy(laserPrinters, printers); + + assertThat(printers.get(0)).isEqualTo(laserPrinter); + } +} diff --git a/src/test/java/techcourse/jcf/mission/README.md b/src/test/java/techcourse/jcf/mission/README.md new file mode 100644 index 0000000000..54a98b388f --- /dev/null +++ b/src/test/java/techcourse/jcf/mission/README.md @@ -0,0 +1,25 @@ +## 제네릭 이해하기 + +### 미션 1 + +기존 SimpleList는 String 타입의 데이터만 다룰 수 있다. +제네릭을 사용하여 String 이외의 타입도 다룰 수 있도록 개선해본다. + +### 미션 2 + +배열을 받아 SimpleList로 변환하는 메서드를 구현해본다. +제네릭을 사용하여 외부에서 생성되는 리스트의 타입을 지정할 수 있어야 한다. + +### 미션 3 + +숫자 타입의 SimpleList를 받아 모든 값을 더해주는 메서드를 구현해본다. +String과 같은 숫자가 아닌 타입이 들어올 경우 예외가 발생해야 한다. + +### 미션 4 + +숫자 타입의 SimpleList를 받아 음수를 제외하고 반환하는 메서드를 구현해본다. + +### 미션 5 + +리스트의 값을 다른 리스트로 복사하는 메서드를 구현해본다. +해당 메서드는 같은 타입이 아닌 상속 관계의 타입도 복사가 가능해야 한다. diff --git a/src/test/java/techcourse/jcf/mission/SimpleArrayList.java b/src/test/java/techcourse/jcf/mission/SimpleArrayList.java new file mode 100644 index 0000000000..ba514bdafb --- /dev/null +++ b/src/test/java/techcourse/jcf/mission/SimpleArrayList.java @@ -0,0 +1,166 @@ +package techcourse.jcf.mission; + +import static java.text.MessageFormat.format; + +import java.util.Arrays; + +public class SimpleArrayList implements SimpleList { + + private static final int DEFAULT_CAPACITY = 10; + + private Object[] values; + private int size; + + public SimpleArrayList() { + this(DEFAULT_CAPACITY); + } + + public SimpleArrayList(final int capacity) { + validateCapacity(capacity); + initCapacity(capacity); + this.size = 0; + } + + private void validateCapacity(final int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException(format("용량은 음수가 될 수 없습니다. 입력: {0}", capacity)); + } + } + + private void initCapacity(final int capacity) { + if (capacity < DEFAULT_CAPACITY) { + this.values = new Object[DEFAULT_CAPACITY]; + } else { + this.values = new Object[capacity]; + } + } + + @Override + public boolean add(final T value) { + if (size == values.length) { + values = expand(); + } + addElement(size, value); + return true; + } + + private Object[] expand() { + final int expandCapacity = calculateExpandCapacity(values.length); + return Arrays.copyOf(values, expandCapacity); + } + + private int calculateExpandCapacity(final int capacity) { + return capacity + (int) (capacity * 0.5); + } + + private void addElement(final int index, final T value) { + values[index] = value; + size++; + } + + @Override + public void add(final int index, final T value) { + validateIndexForAdd(index); + if (size == values.length) { + values = expand(); + } + shiftRight(index, size - 1); + addElement(index, value); + } + + private void validateIndexForAdd(final int index) { + if (index < 0 || size < index) { + throw new IndexOutOfBoundsException(format("해당 인덱스에서 작업을 수행할 수 없습니다. 인덱스: {0}, 크기: {1}", index, size)); + } + } + + private void shiftRight(final int startIndex, final int endIndex) { + for (int i = endIndex; i >= startIndex; i--) { + values[i + 1] = values[i]; + } + } + + @Override + public T set(final int index, final T value) { + validateIndex(index); + final T oldValue = (T) values[index]; + values[index] = value; + return oldValue; + } + + private void validateIndex(final int index) { + if (index < 0 || size <= index) { + throw new IndexOutOfBoundsException(format("해당 인덱스에서 작업을 수행할 수 없습니다. 인덱스: {0}, 크기: {1}", index, size)); + } + } + + @Override + public T get(final int index) { + validateIndex(index); + return (T) values[index]; + } + + @Override + public boolean contains(final T value) { + final int index = indexOf(value); + if (index == -1) { + return false; + } + return true; + } + + @Override + public int indexOf(final T value) { + for (int i = 0; i < size; i++) { + if (values[i].equals(value)) { + return i; + } + } + return -1; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public boolean remove(final T value) { + final int index = indexOf(value); + if (index == -1) { + return false; + } + shiftLeft(index + 1, size - 1); + size--; + return true; + } + + private void shiftLeft(final int startIndex, final int endIndex) { + for (int i = startIndex; i <= endIndex; i++) { + values[i - 1] = values[i]; + } + values[endIndex] = null; + } + + @Override + public T remove(final int index) { + validateIndex(index); + final T value = (T) values[index]; + shiftLeft(index + 1, size - 1); + size--; + return value; + } + + @Override + public void clear() { + for (int i = 0; i < size; i++) { + values[i] = null; + } + size = 0; + } +} diff --git a/src/test/java/techcourse/jcf/mission/SimpleList.java b/src/test/java/techcourse/jcf/mission/SimpleList.java new file mode 100644 index 0000000000..6930c899e6 --- /dev/null +++ b/src/test/java/techcourse/jcf/mission/SimpleList.java @@ -0,0 +1,58 @@ +package techcourse.jcf.mission; + +public interface SimpleList { + + static SimpleList fromArrayToList(final T[] values) { + final SimpleArrayList result = new SimpleArrayList<>(); + for (final T value : values) { + result.add(value); + } + return result; + } + + static double sum(final SimpleList list) { + double sum = 0; + for (int i = 0; i < list.size(); i++) { + sum += list.get(i).doubleValue(); + } + return sum; + } + + static SimpleList filterNegative(final SimpleList list) { + final SimpleList result = new SimpleArrayList<>(); + for (int i = 0; i < list.size(); i++) { + if (list.get(i).doubleValue() >= 0) { + result.add(list.get(i)); + } + } + return result; + } + + static void copy(final SimpleList origin, final SimpleList copy) { + for (int i = 0; i < origin.size(); i++) { + copy.add(origin.get(i)); + } + } + + boolean add(T value); + + void add(int index, T value); + + T set(int index, T value); + + T get(int index); + + boolean contains(T value); + + int indexOf(T value); + + int size(); + + boolean isEmpty(); + + boolean remove(T value); + + T remove(int index); + + void clear(); +}