diff --git a/README.md b/README.md index 556099c4de..c2122fd785 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,49 @@ # java-blackjack -블랙잭 미션 저장소 +## 블랙잭 용어 정리 +- 버스트: 숫자 카드의 합이 21 초과 +- 블랙잭: 숫자 카드 합이 21 + 카드 수가 2장 +- 푸시: 딜러와 플레이어 카드 동점 (=비김) +- 스테이: 카드를 그만 받겠다 +- 힛: 카드를 한장 더 받겠다 + +## 구현 기능 목록 + +- [x] 참여할 사람의 이름 입력 + - 쉼표 기준으로 분리 + - (e) 빈 값, 공백 불가 + - (e) 중복 불가 + - (e) 참가 인원은 최대 8명 +- [x] 카드 분배 + - 딜러, 참가자에게 카드 2장씩 분배 + - 딜러의 카드 1장 출력 + - 참가자의 카드 2장 출력 + - 참가자의 카드 합이 21이면 게임 종료 +- [x] 참가자 카드 추가 분배 + - 카드 합이 21 이상인 참가자는 턴 종료 + - 참가자 별로 한장의 카드 추가 여부를 입력 받기 (y/n) + - 참가자가 카드 추가 여부 입력 + - y를 선택한 경우 카드 추가 분배 + - n를 선택한 경우 해당 참가자의 턴 종료 + - 참가자의 카드 목록 출력 +- [x] 딜러 카드 추가 분배 + - 16 이하면 1장 추가 분배 + - 17 이상이면 턴 종료 +- [x] 결과 출력 + - 카드 목록 및 총합 출력 + (ex: 딜러 카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20) + - 최종 승패 계산 + - 최종 승패 출력 + - 딜러 %d승 %d무 %d패 출력 + - 참가자 승/무/패 결과 출력 + +## 기능 요구 사항 + +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. +- 게임을 완료한 후 각 플레이어별로 승패를 출력한다. ## 우아한테크코스 코드리뷰 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..b2b34dcaa9 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,8 @@ +import blackjack.game.ConsoleGame; + +public class Application { + public static void main(String[] args) { + ConsoleGame consoleGame = new ConsoleGame(); + consoleGame.run(); + } +} 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..b1ae327545 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Card.java @@ -0,0 +1,51 @@ +package blackjack.domain.card; + +import java.util.Objects; + +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 Number getDenomination() { + return number; + } + + public Suit getSuit() { + return suit; + } + + public int toInt() { + return this.number.getValue(); + } + + public boolean isAce() { + return number.isAce(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Card card = (Card) o; + return number == card.number && suit == card.suit; + } + + @Override + public int hashCode() { + return Objects.hash(number, suit); + } + + @Override + public String toString() { + return "Card{" + + "denomination=" + number + + ", suit=" + suit + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/card/CardDistributor.java b/src/main/java/blackjack/domain/card/CardDistributor.java new file mode 100644 index 0000000000..4b348c3d8c --- /dev/null +++ b/src/main/java/blackjack/domain/card/CardDistributor.java @@ -0,0 +1,45 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; + +public class CardDistributor { + + private static final String DECK_IS_EMPTY = "카드가 모두 소요됐습니다."; + private static final List CACHE = new ArrayList<>(); + + private final Stack deck = new Stack<>(); + + static { + for (Suit suit : Suit.values()) { + for (Number number : Number.values()) { + CACHE.add(new Card(number, suit)); + } + } + } + + public CardDistributor() { + Collections.shuffle(CACHE); + deck.addAll(CACHE); + } + + public Card distribute() { + if (isEmpty()) { + throw new IllegalStateException(DECK_IS_EMPTY); + } + return deck.pop(); + } + + private boolean isEmpty() { + return deck.isEmpty(); + } + + @Override + public String toString() { + return "CardDistributor{" + + "deck=" + deck + + '}'; + } +} 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..2e4d4eaa62 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Cards.java @@ -0,0 +1,66 @@ +package blackjack.domain.card; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Cards { + + private static final int ACE_ADDITIONAL_VALUE = 10; + protected static final int BLACKJACK_VALUE = 21; + protected static final int BLACKJACK_COUNT = 2; + + private final List value; + + public Cards(List cards) { + this.value = new ArrayList<>(cards); + } + + public void add(Card card) { + this.value.add(card); + } + + public Status getStatus() { + return Status.findStatus(this); + } + + public int getCount() { + return value.size(); + } + + public int sum() { + int sum = value.stream() + .mapToInt(Card::toInt) + .sum(); + + if (canAddAddtionalValue(sum)) { + sum += ACE_ADDITIONAL_VALUE; + } + + return sum; + } + + private boolean canAddAddtionalValue(int sum) { + return hasAce() && !exceedBust(sum); + } + + private boolean hasAce() { + return value.stream() + .anyMatch(Card::isAce); + } + + private boolean exceedBust(int sum) { + return sum + ACE_ADDITIONAL_VALUE > BLACKJACK_VALUE; + } + + public List getValue() { + return Collections.unmodifiableList(value); + } + + @Override + public String toString() { + return "Cards{" + + "value=" + value + + '}'; + } +} 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..5a58ccf536 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Number.java @@ -0,0 +1,38 @@ +package blackjack.domain.card; + +public enum Number { + + ACE(1, "A"), + 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(10, "J"), + QUEEN(10, "Q"), + KING(10, "K"); + + private final int value; + private final String name; + + Number(int value, String name) { + this.value = value; + this.name = name; + } + + public boolean isAce() { + return this == ACE; + } + + public int getValue() { + return value; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/card/Status.java b/src/main/java/blackjack/domain/card/Status.java new file mode 100644 index 0000000000..a92cb3e4c7 --- /dev/null +++ b/src/main/java/blackjack/domain/card/Status.java @@ -0,0 +1,24 @@ +package blackjack.domain.card; + +import java.util.Arrays; +import java.util.function.Predicate; + +public enum Status { + BLACKJACK((cards) -> cards.getCount() == Cards.BLACKJACK_COUNT && cards.sum() == Cards.BLACKJACK_VALUE), + BUST((cards) -> cards.sum() > Cards.BLACKJACK_VALUE), + NONE((cards) -> cards.sum() < Cards.BLACKJACK_VALUE + || (cards.sum() == Cards.BLACKJACK_VALUE && cards.getCount() != Cards.BLACKJACK_COUNT)); + + private final Predicate condition; + + Status(Predicate condition) { + this.condition = condition; + } + + public static Status findStatus(Cards cards) { + return Arrays.stream(Status.values()) + .filter(status -> status.condition.test(cards)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 cards 입니다.")); + } +} 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..46048d31fa --- /dev/null +++ b/src/main/java/blackjack/domain/card/Suit.java @@ -0,0 +1,19 @@ +package blackjack.domain.card; + +public enum Suit { + + CLOVER("클로버"), + HEART("하트"), + DIAMOND("다이아몬드"), + SPADE("스페이드"); + + private final String name; + + Suit(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/blackjack/domain/game/BlackjackGame.java b/src/main/java/blackjack/domain/game/BlackjackGame.java new file mode 100644 index 0000000000..36d320bb00 --- /dev/null +++ b/src/main/java/blackjack/domain/game/BlackjackGame.java @@ -0,0 +1,62 @@ +package blackjack.domain.game; + +import blackjack.domain.card.Card; +import blackjack.domain.card.CardDistributor; +import blackjack.domain.card.Cards; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Name; +import blackjack.domain.participant.Participant; +import blackjack.domain.participant.Participants; +import blackjack.domain.participant.Player; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BlackjackGame { + + public static final String DEALER_NAME = "딜러"; + private static final int INIT_CARD_COUNT = 2; + + private final Participants participants; + private final CardDistributor cardDistributor = new CardDistributor(); + + public BlackjackGame(List names) { + Dealer dealer = new Dealer(new Name(DEALER_NAME), drawInitialCards()); + List players = initializePlayers(new ArrayList<>(names)); + this.participants = new Participants(players, dealer); + } + + private List initializePlayers(List names) { + return names.stream() + .map(name -> new Player(name, drawInitialCards())) + .collect(Collectors.toUnmodifiableList()); + } + + private Cards drawInitialCards() { + List cards = new ArrayList<>(); + for (int i = 0; i < INIT_CARD_COUNT; i++) { + cards.add(cardDistributor.distribute()); + } + return new Cards(cards); + } + + public void drawCard(Participant participant) { + participants.drawCard(participant, cardDistributor.distribute()); + } + + public GameResult createGameResult() { + return new GameResult(participants.getPlayers(), participants.getDealer()); + } + + public Participants getParticipants() { + return participants; + } + + @Override + public String toString() { + return "BlackjackGame{" + + "participants=" + participants + + ", cardDistributor=" + cardDistributor + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/game/GameResult.java b/src/main/java/blackjack/domain/game/GameResult.java new file mode 100644 index 0000000000..7d1e1bcbdf --- /dev/null +++ b/src/main/java/blackjack/domain/game/GameResult.java @@ -0,0 +1,86 @@ +package blackjack.domain.game; + +import blackjack.domain.card.Cards; +import blackjack.domain.card.Status; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Player; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class GameResult { + + private final Map gameResult = new LinkedHashMap<>(); + + public GameResult(List players, Dealer dealer) { + initGameResult(new ArrayList<>(players), dealer); + } + + private void initGameResult(List players, Dealer dealer) { + for (Player player : players) { + gameResult.put(player, playResult(player, dealer)); + } + } + + private MatchResult playResult(Player player, Dealer dealer) { + Cards playerCards = player.getCards(); + Cards dealerCards = dealer.getCards(); + + if (compareCards(playerCards, dealerCards)) { + return MatchResult.WIN; + } + + if (compareCards(dealerCards, playerCards)) { + return MatchResult.LOSE; + } + + return MatchResult.PUSH; + } + + /** + * 파라미터로 주어진 카드 목록 두 개를 비교하고, 첫 번째 카드 목록이 승리할 경우 true 반환. + * + * @param cards1 - 카드 목록, cards2 - cards1과 비교할 카드 목록 + * @return cards1이 승리 조건에 부합하면 true + */ + private boolean compareCards(Cards cards1, Cards cards2) { + Status status1 = cards1.getStatus(); + Status status2 = cards2.getStatus(); + + return (status1 == Status.BLACKJACK && status2 != Status.BLACKJACK) + || (status1 != Status.BUST && status2 == Status.BUST) + || (status1 == Status.NONE && status2 == Status.NONE && cards1.sum() > cards2.sum()); + } + + public MatchResult getMatchResult(Player player) { + return gameResult.get(player); + } + + public long calculateDealerMatchResultCount(MatchResult matchResult) { + long matchCount = getMatchResultCount(matchResult); + + if (matchResult == MatchResult.PUSH) { + return matchCount; + } + return gameResult.size() - matchCount; + } + + private long getMatchResultCount(MatchResult matchResult) { + return gameResult.entrySet().stream() + .filter(entry -> entry.getValue() == matchResult) + .count(); + } + + public Map getGameResult() { + return Collections.unmodifiableMap(gameResult); + } + + @Override + public String toString() { + return "GameResult{" + + "gameResult=" + gameResult + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/game/MatchResult.java b/src/main/java/blackjack/domain/game/MatchResult.java new file mode 100644 index 0000000000..5fbc04eb87 --- /dev/null +++ b/src/main/java/blackjack/domain/game/MatchResult.java @@ -0,0 +1,18 @@ +package blackjack.domain.game; + +public enum MatchResult { + + WIN("승"), + PUSH("무"), + LOSE("패"); + + private final String value; + + MatchResult(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} 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..aa04b118d7 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Dealer.java @@ -0,0 +1,25 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Cards; + +public class Dealer extends Participant { + + public static final int DRAW_STANDARD = 16; + + public Dealer(Name name, Cards cards) { + super(name, cards); + } + + @Override + public boolean isFinished() { + return cards.sum() > DRAW_STANDARD; + } + + @Override + public String toString() { + return "Dealer{" + + "name=" + name + + ", cards=" + cards + + '}'; + } +} diff --git a/src/main/java/blackjack/domain/participant/Name.java b/src/main/java/blackjack/domain/participant/Name.java new file mode 100644 index 0000000000..aeb6d181d0 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Name.java @@ -0,0 +1,45 @@ +package blackjack.domain.participant; + +import java.util.Objects; + +public class Name { + + private static final String EMPTY_NAME_ERROR = "빈 값은 이름으로 등록할 수 없습니다."; + + private final String value; + + public Name(final String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(EMPTY_NAME_ERROR); + } + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Name name = (Name) o; + return value.equals(name.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "Name{" + + "value='" + value + '\'' + + '}'; + } +} 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..213c224e4d --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participant.java @@ -0,0 +1,47 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import java.util.Objects; + +public abstract class Participant { + + protected final Name name; + protected final Cards cards; + + public Participant(Name name, Cards cards) { + this.name = name; + this.cards = cards; + } + + public abstract boolean isFinished(); + + public void drawCard(Card card) { + cards.add(card); + } + + public String getName() { + return name.getValue(); + } + + public Cards getCards() { + return cards; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Participant that = (Participant) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} 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..7aaf536e56 --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Participants.java @@ -0,0 +1,46 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Card; +import java.util.Collections; +import java.util.List; + +public class Participants { + private static final int MAX_PLAYER_COUNT = 8; + private static final String ERROR_OVER_PLAYER_COUNT = String.format("게임에 참여하는 최대 인원은 %d명 입니다.", MAX_PLAYER_COUNT); + + private final List players; + private final Dealer dealer; + + public Participants(final List players, final Dealer dealer) { + this.players = Collections.unmodifiableList(players); + this.dealer = dealer; + validatePlayers(players); + } + + private void validatePlayers(List players) { + if (players.size() > MAX_PLAYER_COUNT) { + throw new IllegalArgumentException(ERROR_OVER_PLAYER_COUNT); + } + } + + public void drawCard(Participant participant, Card card) { + if (participant.getClass() == Player.class) { + findPlayer((Player) participant).drawCard(card); + return; + } + dealer.drawCard(card); + } + + public Player findPlayer(Player player) { + int index = players.indexOf(player); + return players.get(index); + } + + public List getPlayers() { + return players; + } + + public Dealer getDealer() { + return dealer; + } +} 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..0c1701b61e --- /dev/null +++ b/src/main/java/blackjack/domain/participant/Player.java @@ -0,0 +1,24 @@ +package blackjack.domain.participant; + +import blackjack.domain.card.Cards; +import blackjack.domain.card.Status; + +public class Player extends Participant { + + public Player(Name name, Cards cards) { + super(name, cards); + } + + @Override + public boolean isFinished() { + return cards.getStatus() != Status.NONE; + } + + @Override + public String toString() { + return "Player{" + + "name=" + name + + ", cards=" + cards + + '}'; + } +} diff --git a/src/main/java/blackjack/game/ConsoleGame.java b/src/main/java/blackjack/game/ConsoleGame.java new file mode 100644 index 0000000000..18f1ea1376 --- /dev/null +++ b/src/main/java/blackjack/game/ConsoleGame.java @@ -0,0 +1,77 @@ +package blackjack.game; + +import blackjack.domain.game.BlackjackGame; +import blackjack.domain.game.GameResult; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Name; +import blackjack.domain.participant.Participant; +import blackjack.domain.participant.Participants; +import blackjack.domain.participant.Player; +import blackjack.view.Command; +import blackjack.view.InputView; +import blackjack.view.OutputView; +import java.util.List; + +public class ConsoleGame { + + public void run() { + BlackjackGame blackjackGame = createBlackjackGame(); + + Participants participants = blackjackGame.getParticipants(); + Dealer dealer = participants.getDealer(); + List players = participants.getPlayers(); + + OutputView.printInitialCards(dealer, players); + + playPlayersTurn(blackjackGame, players); + playDealerTurn(blackjackGame, dealer); + + showGameResult(blackjackGame, dealer, players); + } + + private BlackjackGame createBlackjackGame() { + try { + List playerNames = InputView.inputPlayerNames(); + return new BlackjackGame(playerNames); + } catch (IllegalArgumentException e) { + OutputView.printException(e); + return createBlackjackGame(); + } + } + + private void playPlayersTurn(BlackjackGame blackjackGame, List players) { + for (Player player : players) { + playPlayerTurn(blackjackGame, player); + } + } + + private void playPlayerTurn(BlackjackGame blackjackGame, Player player) { + while (!player.isFinished() && !inputCommand(player).isStay()) { + blackjackGame.drawCard(player); + OutputView.printCards(player); + } + } + + private Command inputCommand(Participant player) { + try { + return InputView.inputWantDraw(player.getName()); + + } catch (IllegalArgumentException e) { + OutputView.printException(e); + return inputCommand(player); + } + } + + private void playDealerTurn(BlackjackGame blackjackGame, Dealer dealer) { + while (!dealer.isFinished()) { + OutputView.printDealerDrawInfo(); + blackjackGame.drawCard(dealer); + } + } + + private void showGameResult(BlackjackGame blackjackGame, Dealer dealer, List players) { + GameResult gameResult = blackjackGame.createGameResult(); + OutputView.printCardsResult(dealer, players); + OutputView.printGameResult(gameResult); + } +} diff --git a/src/main/java/blackjack/view/Command.java b/src/main/java/blackjack/view/Command.java new file mode 100644 index 0000000000..a3e5772c67 --- /dev/null +++ b/src/main/java/blackjack/view/Command.java @@ -0,0 +1,29 @@ +package blackjack.view; + +import java.util.Arrays; + +public enum Command { + HIT("y"), + STAY("n"); + + private final String value; + + Command(String value) { + this.value = value; + } + + public static Command findCommand(String value) { + return Arrays.stream(Command.values()) + .filter(command -> command.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("올바른 명령어를 입력해야합니다.")); + } + + public String getValue() { + return value; + } + + public boolean isStay() { + return this == STAY; + } +} diff --git a/src/main/java/blackjack/view/InputView.java b/src/main/java/blackjack/view/InputView.java new file mode 100644 index 0000000000..e974591c70 --- /dev/null +++ b/src/main/java/blackjack/view/InputView.java @@ -0,0 +1,49 @@ +package blackjack.view; + +import blackjack.domain.participant.Name; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + private static final String NAME_DELIMITER = ","; + + private InputView() { + + } + + public static List inputPlayerNames() { + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String input = scanner.nextLine(); + + List names = toNameList(Arrays.asList(input.split(NAME_DELIMITER, -1))); + checkDistinct(names); + return names; + } + + private static List toNameList(List names) { + return names.stream() + .map(name -> new Name(name.trim())) + .collect(Collectors.toUnmodifiableList()); + } + + private static void checkDistinct(List names) { + long distinctCount = names.stream() + .distinct() + .count(); + + if (distinctCount != names.size()) { + throw new IllegalArgumentException("중복 이름은 불가능합니다."); + } + } + + public static Command inputWantDraw(String name) { + System.out.printf("%n%s는 한장의 카드를 더 받겠습니까?(예는 %s, 아니오는 %s)%n" + , name, Command.HIT.getValue(), Command.STAY.getValue()); + String input = scanner.nextLine().toLowerCase().trim(); + return Command.findCommand(input); + } +} diff --git a/src/main/java/blackjack/view/OutputView.java b/src/main/java/blackjack/view/OutputView.java new file mode 100644 index 0000000000..2343ed9696 --- /dev/null +++ b/src/main/java/blackjack/view/OutputView.java @@ -0,0 +1,88 @@ +package blackjack.view; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.game.BlackjackGame; +import blackjack.domain.game.GameResult; +import blackjack.domain.game.MatchResult; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Participant; +import blackjack.domain.participant.Player; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String JOIN_DELIMITER = ", "; + private static final String ERROR_MESSAGE = "[ERROR] "; + + private OutputView() { + + } + + public static void printInitialCards(Dealer dealer, List players) { + System.out.printf("%n%s와 %s에게 2장의 카드를 나누었습니다.%n", dealer.getName(), getPlayerNames(players)); + System.out.printf("%s: %s%n", dealer.getName(), getCardName(dealer.getCards().getValue().get(0))); + for (Player player : players) { + printCards(player); + } + System.out.println(); + } + + private static String getPlayerNames(List players) { + return players.stream() + .map(Participant::getName) + .collect(Collectors.joining(JOIN_DELIMITER)); + } + + private static String getCardNames(Cards cards) { + return cards.getValue().stream() + .map(OutputView::getCardName) + .collect(Collectors.joining(JOIN_DELIMITER)); + } + + private static String getCardName(Card card) { + return card.getDenomination().getName() + card.getSuit().getName(); + } + + public static void printCards(Participant player) { + System.out.printf("%s카드: %s%n", player.getName(), getCardNames(player.getCards())); + } + + public static void printDealerDrawInfo() { + System.out.printf("%n딜러는 %d이하라 한장의 카드를 더 받았습니다.%n", Dealer.DRAW_STANDARD); + } + + public static void printCardsResult(Dealer dealer, List players) { + System.out.println(); + printCardResult(dealer); + for (Player player : players) { + printCardResult(player); + } + } + + private static void printCardResult(Participant participant) { + System.out.printf("%s 카드: %s - 결과: %d%n", + participant.getName(), getCardNames(participant.getCards()), participant.getCards().sum()); + } + + public static void printGameResult(GameResult gameResult) { + Map map = gameResult.getGameResult(); + + System.out.printf("%n%s: %d승 %d무 %d패%n" + , BlackjackGame.DEALER_NAME + , gameResult.calculateDealerMatchResultCount(MatchResult.WIN) + , gameResult.calculateDealerMatchResultCount(MatchResult.PUSH) + , gameResult.calculateDealerMatchResultCount(MatchResult.LOSE) + ); + + for (Player player : map.keySet()) { + System.out.printf("%s: %s%n", player.getName(), gameResult.getMatchResult(player).getValue()); + } + } + + public static void printException(Exception e) { + System.out.println(ERROR_MESSAGE + e.getMessage()); + } +} diff --git a/src/main/java/rentcar/Avante.java b/src/main/java/rentcar/Avante.java new file mode 100644 index 0000000000..f28b7e9586 --- /dev/null +++ b/src/main/java/rentcar/Avante.java @@ -0,0 +1,28 @@ +package rentcar; + +public class Avante extends Car { + + private static final String NAME = "Avante"; + private static final double PER_LITER = 15; + + private double distance; + + public Avante(double distance) { + this.distance = distance; + } + + @Override + double getDistancePerLiter() { + return PER_LITER; + } + + @Override + double getTripDistance() { + return distance; + } + + @Override + String getName() { + return NAME; + } +} diff --git a/src/main/java/rentcar/Car.java b/src/main/java/rentcar/Car.java new file mode 100644 index 0000000000..557c3af560 --- /dev/null +++ b/src/main/java/rentcar/Car.java @@ -0,0 +1,14 @@ +package rentcar; + +public abstract class Car { + + abstract double getDistancePerLiter(); + + abstract double getTripDistance(); + + abstract String getName(); + + double getChargeQuantity() { + return getTripDistance() / getDistancePerLiter(); + } +} diff --git a/src/main/java/rentcar/K5.java b/src/main/java/rentcar/K5.java new file mode 100644 index 0000000000..769354ac8a --- /dev/null +++ b/src/main/java/rentcar/K5.java @@ -0,0 +1,28 @@ +package rentcar; + +public class K5 extends Car { + + private static final String NAME = "K5"; + private static final double PER_LITER = 13; + + private final double distance; + + public K5(double distance) { + this.distance = distance; + } + + @Override + double getDistancePerLiter() { + return PER_LITER; + } + + @Override + double getTripDistance() { + return distance; + } + + @Override + String getName() { + return NAME; + } +} diff --git a/src/main/java/rentcar/RentCompany.java b/src/main/java/rentcar/RentCompany.java new file mode 100644 index 0000000000..1ad84de3e9 --- /dev/null +++ b/src/main/java/rentcar/RentCompany.java @@ -0,0 +1,34 @@ +package rentcar; + +import java.util.ArrayList; +import java.util.List; + +public class RentCompany { + private static final String NEWLINE = System.getProperty("line.separator"); + + private final List cars = new ArrayList<>(); + + private RentCompany() { + + } + + public static RentCompany create() { + return new RentCompany(); + } + + public void addCar (Car car){ + cars.add(car); + } + + public String generateReport() { + StringBuilder stringBuilder = new StringBuilder(); + for(Car car: cars) { + stringBuilder.append(car.getName()) + .append(" : ") + .append(String.format("%.0f", car.getChargeQuantity())) + .append("리터") + .append(NEWLINE); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/rentcar/Sonata.java b/src/main/java/rentcar/Sonata.java new file mode 100644 index 0000000000..2e3175d8af --- /dev/null +++ b/src/main/java/rentcar/Sonata.java @@ -0,0 +1,28 @@ +package rentcar; + +public class Sonata extends Car { + + private static final String NAME = "Sonata"; + private static final double PER_LITER = 10; + + private final double distance; + + public Sonata(double distance) { + this.distance = distance; + } + + @Override + double getDistancePerLiter() { + return PER_LITER; + } + + @Override + double getTripDistance() { + return distance; + } + + @Override + String getName() { + return NAME; + } +} diff --git a/src/test/java/blackjack/domain/card/CardDistributorTest.java b/src/test/java/blackjack/domain/card/CardDistributorTest.java new file mode 100644 index 0000000000..821015779a --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardDistributorTest.java @@ -0,0 +1,37 @@ +package blackjack.domain.card; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CardDistributorTest { + + @Test + @DisplayName("생성 확인") + void distribute() { + // given + CardDistributor cardDistributor = new CardDistributor(); + + // then + assertThatNoException().isThrownBy(cardDistributor::distribute); + } + + @Test + @DisplayName("카드가 다 소요되면 에러가 발생한다.") + void failed() { + // given + CardDistributor cardDistributor = new CardDistributor(); + + // when + for (int i = 0; i < 52; i++) { + cardDistributor.distribute(); + } + + // then + assertThatThrownBy(cardDistributor::distribute) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("카드가 모두 소요됐습니다."); + } +} 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..29642affed --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardTest.java @@ -0,0 +1,13 @@ +package blackjack.domain.card; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatNoException; + +public class CardTest { + @Test + void create() { + // then + assertThatNoException().isThrownBy(() -> new Card(Number.ACE, Suit.CLOVER)); + } +} 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..3c97ee7795 --- /dev/null +++ b/src/test/java/blackjack/domain/card/CardsTest.java @@ -0,0 +1,74 @@ +package blackjack.domain.card; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CardsTest { + + @ParameterizedTest(name = "[{index}] {0}인 경우 총합은 {1}") + @MethodSource("provideParameters") + @DisplayName("단일 카드 총합 구하기") + void sum(Number number, int expect) { + // given + Cards cards = new Cards(Collections.singletonList(new Card(number, Suit.CLOVER))); + + // then + assertThat(cards.sum()).isEqualTo(expect); + } + + private static Stream provideParameters() { + return Stream.of( + Arguments.arguments(Number.ACE, 11), + Arguments.arguments(Number.TWO, 2), + Arguments.arguments(Number.THREE, 3), + Arguments.arguments(Number.FOUR, 4), + Arguments.arguments(Number.FIVE, 5), + Arguments.arguments(Number.SIX, 6), + Arguments.arguments(Number.SEVEN, 7), + Arguments.arguments(Number.EIGHT, 8), + Arguments.arguments(Number.NINE, 9), + Arguments.arguments(Number.TEN, 10), + Arguments.arguments(Number.JACK, 10), + Arguments.arguments(Number.QUEEN, 10), + Arguments.arguments(Number.KING, 10) + ); + } + + @Test + @DisplayName("ACE 2개인 경우") + void sum2() { + // given + Cards cards = new Cards(Arrays.asList(new Card(Number.ACE, Suit.CLOVER) + , new Card(Number.ACE, Suit.HEART))); + + // then + assertThat(cards.sum()).isEqualTo(12); + } + + @Test + @DisplayName("카드 추가하기") + void add() { + // given + List list = new ArrayList<>(); + list.add(new Card(Number.ACE, Suit.CLOVER)); + Cards cards = new Cards(list); + Card newCard = new Card(Number.JACK, Suit.HEART); + + // when + cards.add(newCard); + + // then + assertThat(cards.getValue()).contains(newCard); + } +} diff --git a/src/test/java/blackjack/domain/game/BlackjackGameTest.java b/src/test/java/blackjack/domain/game/BlackjackGameTest.java new file mode 100644 index 0000000000..014e66b001 --- /dev/null +++ b/src/test/java/blackjack/domain/game/BlackjackGameTest.java @@ -0,0 +1,38 @@ +package blackjack.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import blackjack.domain.participant.Name; +import blackjack.domain.participant.Participant; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BlackjackGameTest { + @Test + @DisplayName("게임 초기화 시 각 플레이어는 2장의 카드를 분배받는다.") + void create() { + // given + List names = Arrays.asList(new Name("pobi"), new Name("jason")); + BlackjackGame blackjackGame = new BlackjackGame(names); + + // when + boolean match = blackjackGame + .getParticipants() + .getPlayers() + .stream() + .mapToInt(this::calculateCardsSize) + .anyMatch(cardSize -> cardSize != 2); + + // then + assertThat(match).isFalse(); + } + + private int calculateCardsSize(Participant participant) { + return participant + .getCards() + .getValue() + .size(); + } +} diff --git a/src/test/java/blackjack/domain/game/GameResultTest.java b/src/test/java/blackjack/domain/game/GameResultTest.java new file mode 100644 index 0000000000..740b771e61 --- /dev/null +++ b/src/test/java/blackjack/domain/game/GameResultTest.java @@ -0,0 +1,74 @@ +package blackjack.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static utils.TestUtil.getCards; + +import blackjack.domain.card.Number; +import blackjack.domain.participant.Dealer; +import blackjack.domain.participant.Name; +import blackjack.domain.participant.Player; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class GameResultTest { + @ParameterizedTest(name = "{0}") + @MethodSource("provideParameters") + @DisplayName("플레이어 승리") + void test(String comment, Player player, Dealer dealer) { + // given + GameResult gameResult = new GameResult(List.of(player), dealer); + + // then + assertThat(gameResult.getMatchResult(player)).isEqualTo(MatchResult.WIN); + } + + private static Stream provideParameters() { + return Stream.of( + Arguments.arguments("플레이어가 버스트가 아니고 딜러가 버스트인 경우", + new Player(new Name("abc"), getCards(Number.ACE, Number.NINE)), + new Dealer(new Name("딜러"), getCards(Number.QUEEN, Number.KING, Number.JACK))), + Arguments.arguments("둘 다 버스트가 아니고 딜러보다 숫자가 높은 경우", + new Player(new Name("abc"), getCards(Number.KING, Number.QUEEN)), + new Dealer(new Name("딜러"), getCards(Number.QUEEN, Number.NINE))), + Arguments.arguments("플레이어만 블랙잭", + new Player(new Name("abc"), getCards(Number.KING, Number.ACE)), + new Dealer(new Name("딜러"), getCards(Number.QUEEN, Number.TEN, Number.ACE))) + ); + } + + @ParameterizedTest(name = "{3} 개수 -> {4}회") + @MethodSource("provideParameters2") + @DisplayName("딜러가 승리한 횟수") + void dealer_count_test(Player player1, Player player2, Dealer dealer, MatchResult result, int expect) { + // given + List players = Arrays.asList(player1, player2); + GameResult gameResult = new GameResult(players, dealer); + + // then + assertThat(gameResult.calculateDealerMatchResultCount(result)).isEqualTo(expect); + } + + private static Stream provideParameters2() { + return Stream.of( + Arguments.arguments( + new Player(new Name("abc1"), getCards(Number.ACE, Number.NINE)), + new Player(new Name("abc2"), getCards(Number.EIGHT, Number.NINE)), + new Dealer(new Name("딜러"), getCards(Number.QUEEN, Number.NINE)), + MatchResult.WIN, + 1 + ), + Arguments.arguments( + new Player(new Name("abc1"), getCards(Number.ACE, Number.NINE)), + new Player(new Name("abc2"), getCards(Number.NINE, Number.NINE)), + new Dealer(new Name("딜러"), getCards(Number.EIGHT, Number.NINE)), + MatchResult.LOSE, + 2 + ) + ); + } +} 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..a1aec81499 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/DealerTest.java @@ -0,0 +1,51 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static utils.TestUtil.getCards; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.card.Number; +import blackjack.domain.card.Suit; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class DealerTest { + @ParameterizedTest(name = "{0}") + @MethodSource("provideParameters") + @DisplayName("턴 강제 종료 여부") + void Dealer(String comment, Cards cards, boolean expect) { + // given + Dealer dealer = new Dealer(new Name("딜러"), cards); + + // then + assertThat(dealer.isFinished()).isEqualTo(expect); + } + + private static Stream provideParameters() { + return Stream.of( + Arguments.arguments("합계 22인 경우 true", getCards(Number.TWO, Number.QUEEN, Number.KING), + true), + Arguments.arguments("합계 17인 경우 true", getCards(Number.SEVEN, Number.QUEEN), true), + Arguments.arguments("합계 15인 경우 false", getCards(Number.QUEEN, Number.FIVE), false) + ); + } + + @Test + @DisplayName("딜러 draw 확인") + void drawCard() { + // given + Dealer dealer = new Dealer(new Name("딜러"), getCards(Number.QUEEN)); + Card newCard = new Card(Number.ACE, Suit.CLOVER); + + // when + dealer.drawCard(newCard); + + // then + assertThat(dealer.getCards().getValue()).contains(newCard); + } +} 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..5e59e09fe4 --- /dev/null +++ b/src/test/java/blackjack/domain/participant/ParticipantsTest.java @@ -0,0 +1,55 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static utils.TestUtil.getCards; + +import blackjack.domain.card.Number; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ParticipantsTest { + private List names; + private Participants participants; + + @BeforeEach + void init() { + names = Arrays.asList(new Name("pobi"), new Name("jason")); + + participants = new Participants(toPlayerList(names), + new Dealer(new Name("딜러"), getCards(Number.SEVEN, Number.QUEEN))); + } + + @Test + @DisplayName("플레이어들 가져오기") + void find_players() { + // given + List expectPlayers = toPlayerList(names); + + // when + List playerNames = participants.getPlayers(); + + // then + assertThat(playerNames).containsAll(expectPlayers); + } + + private List toPlayerList(List names) { + List playerList = new ArrayList<>(); + for (Name name : names) { + playerList.add(new Player(name, getCards(Number.SEVEN, Number.QUEEN))); + } + return playerList; + } + + @Test + @DisplayName("딜러 가져오기") + void find_dealer() { + // then + assertThat(participants + .getDealer()) + .isInstanceOf(Dealer.class); + } +} \ No newline at end of file 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..e511cc675f --- /dev/null +++ b/src/test/java/blackjack/domain/participant/PlayerTest.java @@ -0,0 +1,50 @@ +package blackjack.domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; +import static utils.TestUtil.getCards; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.card.Number; +import blackjack.domain.card.Suit; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class PlayerTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("provideParameters") + @DisplayName("턴 강제 종료 여부") + void player(String comment, Cards cards, boolean expect) { + // given + Player player = new Player(new Name("name"), cards); + + // then + assertThat(player.isFinished()).isEqualTo(expect); + } + + private static Stream provideParameters() { + return Stream.of( + Arguments.arguments("합계 22인 경우 true", getCards(Number.TWO, Number.QUEEN, Number.KING), + true), + Arguments.arguments("합계 20인 경우 false", getCards(Number.QUEEN, Number.KING), false) + ); + } + + @Test + void drawCard() { + // given + Player player = new Player(new Name("name"), getCards(Number.QUEEN)); + Card newCard = new Card(Number.ACE, Suit.CLOVER); + + // when + player.drawCard(newCard); + + // then + assertThat(player.getCards().getValue()).contains(newCard); + } +} diff --git a/src/test/java/rentcar/RentCompanyTest.java b/src/test/java/rentcar/RentCompanyTest.java new file mode 100644 index 0000000000..16b81243e7 --- /dev/null +++ b/src/test/java/rentcar/RentCompanyTest.java @@ -0,0 +1,30 @@ +package rentcar; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RentCompanyTest { + private static final String NEWLINE = System.getProperty("line.separator"); + + @Test + @DisplayName("report 결과 확인") + public void report() throws Exception { + RentCompany company = RentCompany.create(); + company.addCar(new Sonata(150)); + company.addCar(new K5(260)); + company.addCar(new Sonata(120)); + company.addCar(new Avante(300)); + company.addCar(new K5(390)); + + String report = company.generateReport(); + assertThat(report).isEqualTo( + "Sonata : 15리터" + NEWLINE + + "K5 : 20리터" + NEWLINE + + "Sonata : 12리터" + NEWLINE + + "Avante : 20리터" + NEWLINE + + "K5 : 30리터" + NEWLINE + ); + } +} diff --git a/src/test/java/utils/TestUtil.java b/src/test/java/utils/TestUtil.java new file mode 100644 index 0000000000..1526c7ef9d --- /dev/null +++ b/src/test/java/utils/TestUtil.java @@ -0,0 +1,20 @@ +package utils; + +import blackjack.domain.card.Card; +import blackjack.domain.card.Cards; +import blackjack.domain.card.Number; +import blackjack.domain.card.Suit; +import java.util.ArrayList; +import java.util.List; + +public class TestUtil { + + public static Cards getCards(Number... arguments) { + List list = new ArrayList<>(); + for (Number number : arguments) { + list.add(new Card(number, Suit.CLOVER)); + } + return new Cards(list); + } + +}