From b6dc60d1d0b7249a932690f8688136d7ce9193c1 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 10:35:12 +0900 Subject: [PATCH 01/17] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D,=20=EA=B2=8C=EC=9E=84=20=EC=A1=B0=EA=B1=B4,=20?= =?UTF-8?q?=EC=88=98=EC=9D=B5=20=EA=B3=84=EC=82=B0=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3bcfc257847..d28dbfbd757 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,39 @@ # java-blackjack 블랙잭 게임 미션 저장소 -## 우아한테크코스 코드리뷰 -* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file +## 기능 목록 +* [ ] 게임에 참여할 사람의 이름을 입력받는다. + * [ ] 참여자의 이름은 1 ~ 5자로 구성된다. + * [ ] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. + * [ ] 참여자의 이름은 comma(,) 단위로 구분한다. + * [ ] 참여자의 수는 2 ~ 8명이다. +* [ ] 각 참여자의 베팅 금액을 입력받는다. + * [ ] 베팅 금액의 최소 단위는 100으로 제한한다. +* [ ] 딜러와 각 참여자에게 카드를 두 장씩 분배한다. +* [ ] 각 참여자에게 나누어 준 카드를 출력한다. + * [ ] 딜러는 첫 번째 카드를 제외하고 한 장의 카드만 공개한다. + * [ ] 각 참여자는 두 장의 카드를 공개한다. +* 딜러가 블랙잭인지 여부를 판별하고, 블랙잭이면 결과를 출력한다. + * [ ] 참여자가 블랙잭이라면 무승부이다. + * [ ] 참여자가 블랙잭이 아니라면 딜러의 승리이다. +* 참여자 히트 + * [ ] 참여자가 블랙잭, 버스트가 아니라면 추가적으로 카드를 히트할지 여부를 입력받는다. + * [ ] 참여자가 스테이하면 다음 사람 턴으로 넘어간다. + * [ ] 참여자가 히트하면 카드 발급 후 카드를 출력한다. +* 딜러 히트 + * [ ] 딜러는 가진 패의 합이 16 이하라면 반드시 히트해야 한다. +* [ ] 딜러와 참여자가 받은 모든 패를 공개하고, 결과를 출력한다. +* [ ] 각 참여자의 최종 수익을 출력한다. + +## 게임 조건 +* 두 장의 패를 뽑아 합이 21인 경우 블랙잭이다. +* 카드의 합이 21을 초과하는 경우 버스트이다. +* 각 ACE는 1 또는 11로 계산할 수 있다. +* 참여자가 버스트가 되면 딜러의 버스트 여부와 상관없이 참여자가 패배한다. + +### 수익 계산 +* 무승부인 경우 베팅한 금액을 돌려받아 수익이 없다. +* 참여자가 승리한 경우 + * 블랙잭이 아니라면 베팅 금액만큼 수익을 얻는다. + * 블랙잭이라면 베팅 금액의 1.5배의 수익을 얻는다. +* 참여자가 패배하면 베팅한 모든 금액을 잃는다. From a263660eee826bc100ba2173cd79208ef9b2937e Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 13:15:49 +0900 Subject: [PATCH 02/17] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/empty.txt | 0 src/test/java/empty.txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/empty.txt delete mode 100644 src/test/java/empty.txt diff --git a/src/main/java/empty.txt b/src/main/java/empty.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29bb2d..00000000000 From 932a06b30284c9a53f49259cabc1bf7e365e62dd Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 13:16:02 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20Name=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +++--- src/main/java/domain/Name.java | 35 +++++++++++++++++++++++++ src/test/java/domain/NameTest.java | 41 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/main/java/domain/Name.java create mode 100644 src/test/java/domain/NameTest.java diff --git a/README.md b/README.md index d28dbfbd757..273dc3fafc5 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ 블랙잭 게임 미션 저장소 ## 기능 목록 -* [ ] 게임에 참여할 사람의 이름을 입력받는다. - * [ ] 참여자의 이름은 1 ~ 5자로 구성된다. - * [ ] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. - * [ ] 참여자의 이름은 comma(,) 단위로 구분한다. +* [X] 게임에 참여할 사람의 이름을 입력받는다. + * [X] 참여자의 이름은 1 ~ 5자로 구성된다. + * [X] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. + * [X] 참여자의 이름은 comma(,) 단위로 구분한다. * [ ] 참여자의 수는 2 ~ 8명이다. * [ ] 각 참여자의 베팅 금액을 입력받는다. * [ ] 베팅 금액의 최소 단위는 100으로 제한한다. diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java new file mode 100644 index 00000000000..264335b0603 --- /dev/null +++ b/src/main/java/domain/Name.java @@ -0,0 +1,35 @@ +package domain; + +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class Name { + private static final int MAX_LENGTH = 5; + private static final int MIN_LENGTH = 1; + private static final String NAME_DELIMITER = ","; + + private final String name; + + public Name(String name) { + Objects.requireNonNull(name, "이름이 null입니다."); + name = name.trim(); + validateLength(name); + this.name = name; + } + + public static List fromComma(final String names) { + return Stream.of(names.split(NAME_DELIMITER)) + .map(Name::new) + .collect(toList()); + } + + private void validateLength(final String name) { + int length = name.length(); + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new IllegalArgumentException("이름의 길이가 올바르지 않습니다.\nname: " + name); + } + } +} diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java new file mode 100644 index 00000000000..d80216be858 --- /dev/null +++ b/src/test/java/domain/NameTest.java @@ -0,0 +1,41 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NameTest { + @DisplayName("생성자: 1~5자 사이의 이름을 입력받아 인스턴스 생성") + @ValueSource(strings = {"뭐", "hello", " hello ", " 다섯글자야", " hello"}) + @ParameterizedTest + void constructor(final String name) { + assertThat(new Name(name)).isInstanceOf(Name.class); + } + + @DisplayName("생성자: 입력받은 이름이 null이면 예외 발생") + @Test + void constructor_NameIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Name(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("이름이 null입니다"); + } + + @DisplayName("생성자: 길이가 올바르지 않은 이름을 입력받아 예외 발생") + @ValueSource(strings = {"", " ", " ", "다섯자넘어유~"}) + @ParameterizedTest + void constructor_InvalidNameLength_ExceptionThrown(final String name) { + assertThatThrownBy(() -> new Name(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름의 길이가 올바르지 않습니다"); + } + + @DisplayName("fromComma: 콤마 단위로 이름을 입력받아 이름 리스트 생성") + @Test + void fromComma() { + assertThat(Name.fromComma("a, hell, hello, 메롱, the, he")).hasSize(6); + } +} From 79728524e04b8f9631b7f08ba044fb0e0ec73f02 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 13:31:24 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20BettingMoney=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/main/java/domain/BettingMoney.java | 33 ++++++++++++++++++++ src/test/java/domain/BettingMoneyTest.java | 35 ++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/main/java/domain/BettingMoney.java create mode 100644 src/test/java/domain/BettingMoneyTest.java diff --git a/README.md b/README.md index 273dc3fafc5..82168f5ac9c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ * [X] 참여자의 이름은 comma(,) 단위로 구분한다. * [ ] 참여자의 수는 2 ~ 8명이다. * [ ] 각 참여자의 베팅 금액을 입력받는다. - * [ ] 베팅 금액의 최소 단위는 100으로 제한한다. + * [X] 베팅 금액의 최소 단위는 100으로 제한한다. * [ ] 딜러와 각 참여자에게 카드를 두 장씩 분배한다. * [ ] 각 참여자에게 나누어 준 카드를 출력한다. * [ ] 딜러는 첫 번째 카드를 제외하고 한 장의 카드만 공개한다. diff --git a/src/main/java/domain/BettingMoney.java b/src/main/java/domain/BettingMoney.java new file mode 100644 index 00000000000..e2b8dd035d3 --- /dev/null +++ b/src/main/java/domain/BettingMoney.java @@ -0,0 +1,33 @@ +package domain; + +public class BettingMoney { + private static final int MIN_AMOUNT = 100; + private static final int BETTING_UNIT = 100; + private final int amount; + + public BettingMoney(final int amount) { + validate(amount); + this.amount = amount; + } + + private void validate(final int amount) { + validateRange(amount); + validateUnit(amount); + } + + private void validateRange(final int amount) { + if (amount < MIN_AMOUNT) { + throw new IllegalArgumentException("베팅 최소 금액을 충족하지 못했습니다.\namount: " + amount); + } + } + + private void validateUnit(final int amount) { + if (amount % BETTING_UNIT != 0) { + throw new IllegalArgumentException("금액의 단위가 올바르지 않습니다.\namount: " + amount); + } + } + + public int getAmount() { + return amount; + } +} diff --git a/src/test/java/domain/BettingMoneyTest.java b/src/test/java/domain/BettingMoneyTest.java new file mode 100644 index 00000000000..6d7b2459051 --- /dev/null +++ b/src/test/java/domain/BettingMoneyTest.java @@ -0,0 +1,35 @@ +package domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BettingMoneyTest { + @DisplayName("생성자: 금액을 입력받아 인스턴스 생성") + @ValueSource(ints = {100, 1_000, 5_000, 10_500}) + @ParameterizedTest + void constructor(final int amount) { + Assertions.assertThat(new BettingMoney(amount)).isInstanceOf(BettingMoney.class); + } + + @DisplayName("생성자: 최소 베팅 금액을 충족하지 못한 경우 예외 발생") + @ValueSource(ints = {0, 99}) + @ParameterizedTest + void constructor_LackOfAmount_ExceptionThrown(final int amount) { + assertThatThrownBy(() -> new BettingMoney(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("베팅 최소 금액을 충족하지 못했습니다"); + } + + @DisplayName("생성자: 금액의 단위가 못한 경우 예외 발생") + @Test + void constructor_BettingUnitMismatch_ExceptionThrown() { + assertThatThrownBy(() -> new BettingMoney(101)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("금액의 단위가 올바르지 않습니다"); + } +} From 5ada5bc6f8342d5c620c7615fa769a7fac574c7c Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 23:05:24 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20Card=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/card/Card.java | 53 ++++++++++++++++++++ src/main/java/domain/card/Face.java | 31 ++++++++++++ src/main/java/domain/card/Suit.java | 8 +++ src/test/java/domain/card/CardTest.java | 66 +++++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 src/main/java/domain/card/Card.java create mode 100644 src/main/java/domain/card/Face.java create mode 100644 src/main/java/domain/card/Suit.java create mode 100644 src/test/java/domain/card/CardTest.java diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 00000000000..555378032bd --- /dev/null +++ b/src/main/java/domain/card/Card.java @@ -0,0 +1,53 @@ +package domain.card; + +import static java.util.stream.Collectors.toList; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class Card { + private final Face face; + private final Suit suit; + + Card(final Face face, final Suit suit) { + this.face = Objects.requireNonNull(face, "face가 null입니다."); + this.suit = Objects.requireNonNull(suit, "suit가 null입니다."); + } + + public static List values() { + return Collections.unmodifiableList(CardCache.values()); + } + + public boolean isAce() { + return face.isAce(); + } + + public Face getFace() { + return face; + } + + public Suit getSuit() { + return suit; + } + + private static class CardCache { + private static final List cache; + + static { + cache = Stream.of(Face.values()) + .flatMap(CardCache::createByFace) + .collect(toList()); + } + + private static Stream createByFace(final Face face) { + return Stream.of(Suit.values()) + .map(suit -> new Card(face, suit)); + } + + public static List values() { + return cache; + } + } +} diff --git a/src/main/java/domain/card/Face.java b/src/main/java/domain/card/Face.java new file mode 100644 index 00000000000..b95555f2655 --- /dev/null +++ b/src/main/java/domain/card/Face.java @@ -0,0 +1,31 @@ +package domain.card; + +public enum Face { + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10), + QUEEN(10), + KING(10); + + private final int score; + + Face(final int score) { + this.score = score; + } + + public boolean isAce() { + return this == ACE; + } + + public int getScore() { + return score; + } +} diff --git a/src/main/java/domain/card/Suit.java b/src/main/java/domain/card/Suit.java new file mode 100644 index 00000000000..ab184249a3e --- /dev/null +++ b/src/main/java/domain/card/Suit.java @@ -0,0 +1,8 @@ +package domain.card; + +public enum Suit { + SPACE, + HEART, + DIAMOND, + CLUB +} diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java new file mode 100644 index 00000000000..0e653a09ec4 --- /dev/null +++ b/src/test/java/domain/card/CardTest.java @@ -0,0 +1,66 @@ +package domain.card; + +import static domain.card.Face.ACE; +import static domain.card.Suit.CLUB; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class CardTest { + @DisplayName("생성자: 카드 한 장을 생성") + @Test + void constructor() { + assertThat(new Card(ACE, CLUB)).isInstanceOf(Card.class); + } + + @DisplayName("생성자: Face가 null인 경우 예외 발생") + @Test + void constructor_FaceIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Card(null, CLUB)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("face가 null입니다."); + } + + @DisplayName("생성자: Suit가 null인 경우 예외 발생") + @Test + void constructor_SuitIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Card(ACE, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("suit가 null입니다."); + } + + @DisplayName("values: 전체 카드를 반환") + @Test + void values() { + assertThat(Card.values()).hasSize(52); + } + + @DisplayName("isAce: 카드가 Ace인지 판별") + @CsvSource(value = {"ACE, true", "TEN, false"}) + @ParameterizedTest + void isAce(final Face face, final boolean expect) { + Card card = new Card(face, CLUB); + + assertThat(card.isAce()).isEqualTo(expect); + } + + @DisplayName("getFace: face를 반환") + @Test + void getFace() { + Card card = new Card(ACE, CLUB); + + assertThat(card.getFace()).isEqualTo(ACE); + } + + @DisplayName("getSuit: suit를 반환") + @Test + void getSuit() { + Card card = new Card(ACE, CLUB); + + assertThat(card.getSuit()).isEqualTo(CLUB); + } +} From af732049c2fa4e2ae80d45f7baa0e34d71747bb1 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 23:05:45 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20RandomCardDeck=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/card/CardDeck.java | 5 +++ src/main/java/domain/card/RandomCardDeck.java | 30 +++++++++++++ src/test/java/domain/card/CardDeckTest.java | 42 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/main/java/domain/card/CardDeck.java create mode 100644 src/main/java/domain/card/RandomCardDeck.java create mode 100644 src/test/java/domain/card/CardDeckTest.java diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java new file mode 100644 index 00000000000..b67198cfde1 --- /dev/null +++ b/src/main/java/domain/card/CardDeck.java @@ -0,0 +1,5 @@ +package domain.card; + +public interface CardDeck { + Card pick(); +} diff --git a/src/main/java/domain/card/RandomCardDeck.java b/src/main/java/domain/card/RandomCardDeck.java new file mode 100644 index 00000000000..20e8514986e --- /dev/null +++ b/src/main/java/domain/card/RandomCardDeck.java @@ -0,0 +1,30 @@ +package domain.card; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Stack; + +public class RandomCardDeck implements CardDeck { + private final Stack cards; + + public RandomCardDeck(final List cards) { + validate(cards); + Stack shuffledCards = new Stack<>(); + shuffledCards.addAll(cards); + Collections.shuffle(shuffledCards); + this.cards = shuffledCards; + } + + @Override + public Card pick() { + return cards.pop(); + } + + private void validate(final List cards) { + Objects.requireNonNull(cards, "카드 리스트가 null입니다."); + if (cards.isEmpty()) { + throw new IllegalArgumentException("카드 리스트의 크기가 0입니다."); + } + } +} diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java new file mode 100644 index 00000000000..244a075a13b --- /dev/null +++ b/src/test/java/domain/card/CardDeckTest.java @@ -0,0 +1,42 @@ +package domain.card; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CardDeckTest { + @DisplayName("생성자: 카드 덱 생성") + @Test + void constructor() { + Assertions.assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); + + } + + @DisplayName("생성자: 카드 리스트가 null인 경우 예외 발생") + @Test + void constructor_CardsIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new RandomCardDeck(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("카드 리스트가 null입니다"); + } + + @DisplayName("생성자: 카드 리스트의 크기가 0인 경우 예외 발생") + @Test + void constructor_CardsIsEmpty_ExceptionThrown() { + assertThatThrownBy(() -> new RandomCardDeck(new ArrayList<>())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("카드 리스트의 크기가 0입니다"); + } + + @DisplayName("pick: 카드 한 장을 뽑음") + @Test + void pick() { + RandomCardDeck cardDeck = new RandomCardDeck(Card.values()); + + Assertions.assertThat(cardDeck.pick()).isInstanceOf(Card.class); + } +} From 8fb0521cba3d89717647fd52e5df450d873d37a1 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Tue, 30 Jun 2020 23:28:19 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20Player=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/Name.java | 4 +++ src/main/java/domain/participant/Player.java | 16 +++++++++ src/test/java/domain/BettingMoneyTest.java | 4 +-- src/test/java/domain/Fixture.java | 15 +++++++++ src/test/java/domain/card/CardDeckTest.java | 3 +- .../java/domain/participant/PlayerTest.java | 33 +++++++++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/main/java/domain/participant/Player.java create mode 100644 src/test/java/domain/Fixture.java create mode 100644 src/test/java/domain/participant/PlayerTest.java diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java index 264335b0603..194150efb9a 100644 --- a/src/main/java/domain/Name.java +++ b/src/main/java/domain/Name.java @@ -32,4 +32,8 @@ private void validateLength(final String name) { throw new IllegalArgumentException("이름의 길이가 올바르지 않습니다.\nname: " + name); } } + + public String getName() { + return name; + } } diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java new file mode 100644 index 00000000000..67fcbb87727 --- /dev/null +++ b/src/main/java/domain/participant/Player.java @@ -0,0 +1,16 @@ +package domain.participant; + +import java.util.Objects; + +import domain.BettingMoney; +import domain.Name; + +public class Player { + private final Name name; + private final BettingMoney bettingMoney; + + public Player(final Name name, final BettingMoney bettingMoney) { + this.name = Objects.requireNonNull(name, "name이 null입니다."); + this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); + } +} diff --git a/src/test/java/domain/BettingMoneyTest.java b/src/test/java/domain/BettingMoneyTest.java index 6d7b2459051..4b9da972bb8 100644 --- a/src/test/java/domain/BettingMoneyTest.java +++ b/src/test/java/domain/BettingMoneyTest.java @@ -1,8 +1,8 @@ package domain; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -13,7 +13,7 @@ class BettingMoneyTest { @ValueSource(ints = {100, 1_000, 5_000, 10_500}) @ParameterizedTest void constructor(final int amount) { - Assertions.assertThat(new BettingMoney(amount)).isInstanceOf(BettingMoney.class); + assertThat(new BettingMoney(amount)).isInstanceOf(BettingMoney.class); } @DisplayName("생성자: 최소 베팅 금액을 충족하지 못한 경우 예외 발생") diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java new file mode 100644 index 00000000000..82489d99835 --- /dev/null +++ b/src/test/java/domain/Fixture.java @@ -0,0 +1,15 @@ +package domain; + +import java.util.List; + +import domain.card.Card; + +public class Fixture { + public static final List CARDS = Card.values(); + + public static final Name POBI = new Name("Pobi"); + public static final Name JUN = new Name("Jun"); + + public static final BettingMoney THOUSAND_BETTING_MONEY = new BettingMoney(1_000); + public static final BettingMoney HUNDRED_BETTING_MONEY = new BettingMoney(1_000_000); +} diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java index 244a075a13b..4c587de7947 100644 --- a/src/test/java/domain/card/CardDeckTest.java +++ b/src/test/java/domain/card/CardDeckTest.java @@ -1,5 +1,6 @@ package domain.card; +import static domain.Fixture.CARDS; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; @@ -35,7 +36,7 @@ void constructor_CardsIsEmpty_ExceptionThrown() { @DisplayName("pick: 카드 한 장을 뽑음") @Test void pick() { - RandomCardDeck cardDeck = new RandomCardDeck(Card.values()); + RandomCardDeck cardDeck = new RandomCardDeck(CARDS); Assertions.assertThat(cardDeck.pick()).isInstanceOf(Card.class); } diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java new file mode 100644 index 00000000000..281d48f2fc4 --- /dev/null +++ b/src/test/java/domain/participant/PlayerTest.java @@ -0,0 +1,33 @@ +package domain.participant; + +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.JUN; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PlayerTest { + @DisplayName("생성자: 사용자 생성") + @Test + void constructor() { + Assertions.assertThat(new Player(JUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + } + + @DisplayName("생성자: Name이 null이면 예외 발생") + @Test + void constructor_NameIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(null, HUNDRED_BETTING_MONEY)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("name이 null입니다"); + } + + @DisplayName("생성자: BettingMoney가 null이면 예외 발생") + @Test + void constructor_BettingMoneyIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(JUN, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("bettingMoney가 null입니다"); + } +} From 99cf6c3c3fc74988301f41741953f2f8ed15b18e Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Wed, 1 Jul 2020 16:35:20 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20Hand=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/card/Card.java | 29 +++-- .../domain/card/CardNotFoundException.java | 7 ++ src/main/java/domain/card/Suit.java | 2 +- .../java/domain/participant/hand/Hand.java | 62 ++++++++++ src/test/java/domain/Fixture.java | 30 +++++ src/test/java/domain/card/CardTest.java | 42 +++---- .../domain/participant/hand/HandTest.java | 108 ++++++++++++++++++ 7 files changed, 249 insertions(+), 31 deletions(-) create mode 100644 src/main/java/domain/card/CardNotFoundException.java create mode 100644 src/main/java/domain/participant/hand/Hand.java create mode 100644 src/test/java/domain/participant/hand/HandTest.java diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java index 555378032bd..c972f79eace 100644 --- a/src/main/java/domain/card/Card.java +++ b/src/main/java/domain/card/Card.java @@ -4,26 +4,39 @@ import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.stream.Stream; public class Card { private final Face face; private final Suit suit; - Card(final Face face, final Suit suit) { - this.face = Objects.requireNonNull(face, "face가 null입니다."); - this.suit = Objects.requireNonNull(suit, "suit가 null입니다."); + private Card(final Face face, final Suit suit) { + this.face = face; + this.suit = suit; + } + + public static Card fromFaceAndSuit(final Face face, final Suit suit) { + return CardCache.cache + .stream() + .filter(card -> card.isCardOf(face, suit)) + .findFirst() + .orElseThrow(() -> new CardNotFoundException("카드가 존재하지 않습니다.\n" + + "face: " + face + "\n" + + "suit: " + suit)); } public static List values() { - return Collections.unmodifiableList(CardCache.values()); + return Collections.unmodifiableList(CardCache.cache); } public boolean isAce() { return face.isAce(); } + public boolean isCardOf(final Face face, final Suit suit) { + return this.face == face && this.suit == suit; + } + public Face getFace() { return face; } @@ -33,7 +46,7 @@ public Suit getSuit() { } private static class CardCache { - private static final List cache; + public static final List cache; static { cache = Stream.of(Face.values()) @@ -45,9 +58,5 @@ private static Stream createByFace(final Face face) { return Stream.of(Suit.values()) .map(suit -> new Card(face, suit)); } - - public static List values() { - return cache; - } } } diff --git a/src/main/java/domain/card/CardNotFoundException.java b/src/main/java/domain/card/CardNotFoundException.java new file mode 100644 index 00000000000..cd5bcd4a65c --- /dev/null +++ b/src/main/java/domain/card/CardNotFoundException.java @@ -0,0 +1,7 @@ +package domain.card; + +public class CardNotFoundException extends RuntimeException { + public CardNotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/domain/card/Suit.java b/src/main/java/domain/card/Suit.java index ab184249a3e..9015751f019 100644 --- a/src/main/java/domain/card/Suit.java +++ b/src/main/java/domain/card/Suit.java @@ -1,7 +1,7 @@ package domain.card; public enum Suit { - SPACE, + SPADE, HEART, DIAMOND, CLUB diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java new file mode 100644 index 00000000000..8557ac89d5c --- /dev/null +++ b/src/main/java/domain/participant/hand/Hand.java @@ -0,0 +1,62 @@ +package domain.participant.hand; + +import java.util.ArrayList; +import java.util.List; + +import domain.card.Card; + +public class Hand { + private static final int BLACKJACK_CARD_SIZE = 2; + private static final int BLACKJACK_SCORE = 21; + private static final int ACE_UPGRADABLE_SCORE_UPPER_BOUND = 11; + private static final int ACE_UPGRADE_SCORE = 10; + + private final List cards; + + public Hand() { + this.cards = new ArrayList<>(); + } + + public void draw(final Card card) { + cards.add(card); + } + + public boolean isBlackjack() { + return cards.size() == BLACKJACK_CARD_SIZE && calculateScore() == BLACKJACK_SCORE; + } + + public boolean isBust() { + return calculateScore() > BLACKJACK_SCORE; + } + + public int calculateScore() { + int score = calculateMaxScore(); + return upgradeIfHasAce(score); + } + + private int calculateMaxScore() { + return cards.stream() + .mapToInt(card -> card.getFace().getScore()) + .sum(); + } + + private int upgradeIfHasAce(final int score) { + if (hasAce() && isAceUpgradableScore(score)) { + return score + ACE_UPGRADE_SCORE; + } + return score; + } + + private boolean hasAce() { + return cards.stream() + .anyMatch(Card::isAce); + } + + private boolean isAceUpgradableScore(final int score) { + return score <= ACE_UPGRADABLE_SCORE_UPPER_BOUND; + } + + public List getCards() { + return cards; + } +} diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index 82489d99835..0096e64258c 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -1,5 +1,17 @@ package domain; +import static domain.card.Face.ACE; +import static domain.card.Face.FIVE; +import static domain.card.Face.FOUR; +import static domain.card.Face.JACK; +import static domain.card.Face.SEVEN; +import static domain.card.Face.SIX; +import static domain.card.Face.THREE; +import static domain.card.Face.TWO; +import static domain.card.Suit.CLUB; +import static domain.card.Suit.SPADE; + +import java.util.Arrays; import java.util.List; import domain.card.Card; @@ -12,4 +24,22 @@ public class Fixture { public static final BettingMoney THOUSAND_BETTING_MONEY = new BettingMoney(1_000); public static final BettingMoney HUNDRED_BETTING_MONEY = new BettingMoney(1_000_000); + + public static final Card ACE_SCORE = Card.fromFaceAndSuit(ACE, CLUB); + public static final Card TWO_SCORE = Card.fromFaceAndSuit(TWO, CLUB); + public static final Card THREE_SCORE = Card.fromFaceAndSuit(THREE, CLUB); + public static final Card FOUR_SCORE = Card.fromFaceAndSuit(FOUR, CLUB); + public static final Card FIVE_SCORE = Card.fromFaceAndSuit(FIVE, CLUB); + public static final Card SIX_SCORE = Card.fromFaceAndSuit(SIX, CLUB); + public static final Card SEVEN_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); + public static final Card EIGHT_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); + public static final Card NINE_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); + public static final Card TEN_SCORE = Card.fromFaceAndSuit(JACK, SPADE); + + public static final List BUSTED_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, TWO_SCORE); + public static final List BUSTED_BY_ACE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE, ACE_SCORE); + public static final List BLACKJACK_CARDS = Arrays.asList(TEN_SCORE, ACE_SCORE); + public static final List MAX_SCORE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE); + public static final List DEALER_HITTABLE_UPPER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SIX_SCORE); + public static final List DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SEVEN_SCORE); } diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java index 0e653a09ec4..89e9982c630 100644 --- a/src/test/java/domain/card/CardTest.java +++ b/src/test/java/domain/card/CardTest.java @@ -11,26 +11,19 @@ import org.junit.jupiter.params.provider.CsvSource; public class CardTest { - @DisplayName("생성자: 카드 한 장을 생성") + @DisplayName("fromFaceAndSuit: 카드 한 장을 생성") @Test - void constructor() { - assertThat(new Card(ACE, CLUB)).isInstanceOf(Card.class); + void fromFaceAndSuit() { + assertThat(Card.fromFaceAndSuit(ACE, CLUB)).isInstanceOf(Card.class); } - @DisplayName("생성자: Face가 null인 경우 예외 발생") - @Test - void constructor_FaceIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Card(null, CLUB)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("face가 null입니다."); - } - - @DisplayName("생성자: Suit가 null인 경우 예외 발생") - @Test - void constructor_SuitIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Card(ACE, null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("suit가 null입니다."); + @DisplayName("fromFaceAndSuit: face 또는 suit가 null인 경우 예외 발생") + @CsvSource(value = {", CLUB", "TEN,"}) + @ParameterizedTest + void fromFaceAndSuit_FaceOrSuitIsNull_ExceptionThrown(final Face face, final Suit suit) { + assertThatThrownBy(() -> Card.fromFaceAndSuit(face, suit)) + .isInstanceOf(CardNotFoundException.class) + .hasMessageContaining("카드가 존재하지 않습니다"); } @DisplayName("values: 전체 카드를 반환") @@ -43,15 +36,24 @@ void values() { @CsvSource(value = {"ACE, true", "TEN, false"}) @ParameterizedTest void isAce(final Face face, final boolean expect) { - Card card = new Card(face, CLUB); + Card card = Card.fromFaceAndSuit(face, CLUB); assertThat(card.isAce()).isEqualTo(expect); } + @DisplayName("isCardOf: 카드의 face와 suit가 일치하는지 확인") + @CsvSource(value = {"ACE, CLUB, true", "TEN, CLUB, false", "ACE, DIAMOND, false"}) + @ParameterizedTest + void isAce(final Face face, final Suit suit, final boolean expect) { + Card card = Card.fromFaceAndSuit(ACE, CLUB); + + assertThat(card.isCardOf(face, suit)).isEqualTo(expect); + } + @DisplayName("getFace: face를 반환") @Test void getFace() { - Card card = new Card(ACE, CLUB); + Card card = Card.fromFaceAndSuit(ACE, CLUB); assertThat(card.getFace()).isEqualTo(ACE); } @@ -59,7 +61,7 @@ void getFace() { @DisplayName("getSuit: suit를 반환") @Test void getSuit() { - Card card = new Card(ACE, CLUB); + Card card = Card.fromFaceAndSuit(ACE, CLUB); assertThat(card.getSuit()).isEqualTo(CLUB); } diff --git a/src/test/java/domain/participant/hand/HandTest.java b/src/test/java/domain/participant/hand/HandTest.java new file mode 100644 index 00000000000..f04bf117601 --- /dev/null +++ b/src/test/java/domain/participant/hand/HandTest.java @@ -0,0 +1,108 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.BUSTED_BY_ACE_CARDS; +import static domain.Fixture.BUSTED_CARDS; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS; +import static domain.Fixture.MAX_SCORE_CARDS; +import static domain.card.Face.ACE; +import static domain.card.Suit.SPADE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import domain.card.Card; + +public class HandTest { + private static Stream createCardsAndScore() { + return Stream.of( + Arguments.of(BUSTED_CARDS, 22), + Arguments.of(BUSTED_BY_ACE_CARDS, 22), + Arguments.of(BLACKJACK_CARDS, 21), + Arguments.of(MAX_SCORE_CARDS, 21), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, 16), + Arguments.of(DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS, 17) + ); + } + + private static Stream createCardsAndIsBlackjack() { + return Stream.of( + Arguments.of(BUSTED_BY_ACE_CARDS, false), + Arguments.of(BLACKJACK_CARDS, true), + Arguments.of(MAX_SCORE_CARDS, false), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, false) + ); + } + + private static Stream createCardsAndIsBust() { + return Stream.of( + Arguments.of(BUSTED_BY_ACE_CARDS, true), + Arguments.of(BLACKJACK_CARDS, false), + Arguments.of(MAX_SCORE_CARDS, false), + Arguments.of(DEALER_HITTABLE_UPPER_BOUND_CARDS, false) + ); + } + + @DisplayName("생성자: Hand 인스턴스 생성") + @Test + void constructor() { + assertThat(new Hand()).isInstanceOf(Hand.class); + } + + @DisplayName("draw: 카드를 한 장 추가") + @Test + void draw() { + Hand hand = new Hand(); + Card card = Card.fromFaceAndSuit(ACE, SPADE); + + hand.draw(card); + assertThat(hand.getCards()).hasSize(1); + } + + @DisplayName("isBlackjack: 카드가 블랙잭인지 여부를 판단") + @MethodSource("createCardsAndIsBlackjack") + @ParameterizedTest + void isBlackjack(final List cards, final boolean expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.isBlackjack()).isEqualTo(expect); + } + + @DisplayName("isBust: 카드가 버스트인지 여부를 판단") + @MethodSource("createCardsAndIsBust") + @ParameterizedTest + void isBust(final List cards, final boolean expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.isBust()).isEqualTo(expect); + } + + @DisplayName("calculateScore: 카드의 점수를 계산") + @MethodSource("createCardsAndScore") + @ParameterizedTest + void calculateScore(final List cards, final int expect) { + Hand hand = new Hand(); + + for (final Card card : cards) { + hand.draw(card); + } + + assertThat(hand.calculateScore()).isEqualTo(expect); + } +} From aba0e560dd89d5c3d4719f7ce70f8bd98a8bd88d Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Wed, 1 Jul 2020 16:36:26 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20CardDeck=20=EC=97=AC=EB=9F=AC?= =?UTF-8?q?=EC=9E=A5=20=EB=BD=91=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/card/CardDeck.java | 4 ++++ src/main/java/domain/card/RandomCardDeck.java | 10 +++++++++ ...dDeckTest.java => RandomCardDeckTest.java} | 22 ++++++++++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) rename src/test/java/domain/card/{CardDeckTest.java => RandomCardDeckTest.java} (66%) diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java index b67198cfde1..2d0bc379ea0 100644 --- a/src/main/java/domain/card/CardDeck.java +++ b/src/main/java/domain/card/CardDeck.java @@ -1,5 +1,9 @@ package domain.card; +import java.util.List; + public interface CardDeck { Card pick(); + + List pick(final int count); } diff --git a/src/main/java/domain/card/RandomCardDeck.java b/src/main/java/domain/card/RandomCardDeck.java index 20e8514986e..8c38cbb01a7 100644 --- a/src/main/java/domain/card/RandomCardDeck.java +++ b/src/main/java/domain/card/RandomCardDeck.java @@ -1,5 +1,6 @@ package domain.card; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -21,6 +22,15 @@ public Card pick() { return cards.pop(); } + @Override + public List pick(final int amount) { + List pickedCards = new ArrayList<>(); + for (int count = 0; count < amount; ++count) { + pickedCards.add(pick()); + } + return pickedCards; + } + private void validate(final List cards) { Objects.requireNonNull(cards, "카드 리스트가 null입니다."); if (cards.isEmpty()) { diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/RandomCardDeckTest.java similarity index 66% rename from src/test/java/domain/card/CardDeckTest.java rename to src/test/java/domain/card/RandomCardDeckTest.java index 4c587de7947..d3ce8dd5635 100644 --- a/src/test/java/domain/card/CardDeckTest.java +++ b/src/test/java/domain/card/RandomCardDeckTest.java @@ -1,19 +1,27 @@ package domain.card; import static domain.Fixture.CARDS; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; -import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class CardDeckTest { +public class RandomCardDeckTest { + private RandomCardDeck cardDeck; + + @BeforeEach + void setUp() { + cardDeck = new RandomCardDeck(CARDS); + } + @DisplayName("생성자: 카드 덱 생성") @Test void constructor() { - Assertions.assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); + assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); } @@ -36,8 +44,12 @@ void constructor_CardsIsEmpty_ExceptionThrown() { @DisplayName("pick: 카드 한 장을 뽑음") @Test void pick() { - RandomCardDeck cardDeck = new RandomCardDeck(CARDS); + assertThat(cardDeck.pick()).isInstanceOf(Card.class); + } - Assertions.assertThat(cardDeck.pick()).isInstanceOf(Card.class); + @DisplayName("pick: 카드를 입력받은 장수만큼 뽑음") + @Test + void pick_MultipleCards() { + assertThat(cardDeck.pick(5)).hasSize(5); } } From a288891e25288e8fd50fbbb4be70283306625bd5 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Wed, 1 Jul 2020 17:30:50 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20HandState=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/BettingMoney.java | 15 ++-- src/main/java/domain/Name.java | 3 +- .../participant/hand/BlackjackState.java | 16 +++++ .../domain/participant/hand/BustedState.java | 16 +++++ .../participant/hand/FinishedState.java | 23 ++++++ .../domain/participant/hand/HandState.java | 23 ++++++ .../participant/hand/HandStateFactory.java | 14 ++++ .../domain/participant/hand/HitState.java | 28 ++++++++ .../domain/participant/hand/StartedState.java | 14 ++++ .../domain/participant/hand/StayState.java | 16 +++++ src/test/java/domain/BettingMoneyTest.java | 15 +++- src/test/java/domain/Fixture.java | 9 +++ src/test/java/domain/NameTest.java | 15 +++- src/test/java/domain/card/CardTest.java | 2 +- .../java/domain/card/RandomCardDeckTest.java | 8 +-- .../java/domain/participant/DealerTest.java | 14 ++++ .../java/domain/participant/PlayerTest.java | 28 ++++++-- .../participant/hand/BlackjackStateTest.java | 48 +++++++++++++ .../participant/hand/BustedStateTest.java | 49 +++++++++++++ .../hand/HandStateFactoryTest.java | 29 ++++++++ .../domain/participant/hand/HandTest.java | 4 +- .../domain/participant/hand/HitStateTest.java | 70 +++++++++++++++++++ .../participant/hand/StayStateTest.java | 48 +++++++++++++ 23 files changed, 482 insertions(+), 25 deletions(-) create mode 100644 src/main/java/domain/participant/hand/BlackjackState.java create mode 100644 src/main/java/domain/participant/hand/BustedState.java create mode 100644 src/main/java/domain/participant/hand/FinishedState.java create mode 100644 src/main/java/domain/participant/hand/HandState.java create mode 100644 src/main/java/domain/participant/hand/HandStateFactory.java create mode 100644 src/main/java/domain/participant/hand/HitState.java create mode 100644 src/main/java/domain/participant/hand/StartedState.java create mode 100644 src/main/java/domain/participant/hand/StayState.java create mode 100644 src/test/java/domain/participant/DealerTest.java create mode 100644 src/test/java/domain/participant/hand/BlackjackStateTest.java create mode 100644 src/test/java/domain/participant/hand/BustedStateTest.java create mode 100644 src/test/java/domain/participant/hand/HandStateFactoryTest.java create mode 100644 src/test/java/domain/participant/hand/HitStateTest.java create mode 100644 src/test/java/domain/participant/hand/StayStateTest.java diff --git a/src/main/java/domain/BettingMoney.java b/src/main/java/domain/BettingMoney.java index e2b8dd035d3..2a97c2d390a 100644 --- a/src/main/java/domain/BettingMoney.java +++ b/src/main/java/domain/BettingMoney.java @@ -1,13 +1,16 @@ package domain; +import java.math.BigDecimal; + public class BettingMoney { private static final int MIN_AMOUNT = 100; private static final int BETTING_UNIT = 100; - private final int amount; + + private final BigDecimal amount; public BettingMoney(final int amount) { validate(amount); - this.amount = amount; + this.amount = BigDecimal.valueOf(amount); } private void validate(final int amount) { @@ -17,17 +20,19 @@ private void validate(final int amount) { private void validateRange(final int amount) { if (amount < MIN_AMOUNT) { - throw new IllegalArgumentException("베팅 최소 금액을 충족하지 못했습니다.\namount: " + amount); + throw new IllegalArgumentException("베팅 최소 금액을 충족하지 못했습니다.\n" + + "amount: " + amount); } } private void validateUnit(final int amount) { if (amount % BETTING_UNIT != 0) { - throw new IllegalArgumentException("금액의 단위가 올바르지 않습니다.\namount: " + amount); + throw new IllegalArgumentException("금액의 단위가 올바르지 않습니다.\n" + + "amount: " + amount); } } - public int getAmount() { + public BigDecimal getAmount() { return amount; } } diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java index 194150efb9a..4a29216d044 100644 --- a/src/main/java/domain/Name.java +++ b/src/main/java/domain/Name.java @@ -29,7 +29,8 @@ public static List fromComma(final String names) { private void validateLength(final String name) { int length = name.length(); if (length < MIN_LENGTH || length > MAX_LENGTH) { - throw new IllegalArgumentException("이름의 길이가 올바르지 않습니다.\nname: " + name); + throw new IllegalArgumentException("이름의 길이가 올바르지 않습니다.\n" + + "name: " + name); } } diff --git a/src/main/java/domain/participant/hand/BlackjackState.java b/src/main/java/domain/participant/hand/BlackjackState.java new file mode 100644 index 00000000000..43910b95685 --- /dev/null +++ b/src/main/java/domain/participant/hand/BlackjackState.java @@ -0,0 +1,16 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class BlackjackState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(1.5); + + public BlackjackState(final Hand hand) { + super(hand); + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } +} diff --git a/src/main/java/domain/participant/hand/BustedState.java b/src/main/java/domain/participant/hand/BustedState.java new file mode 100644 index 00000000000..85a2c145d85 --- /dev/null +++ b/src/main/java/domain/participant/hand/BustedState.java @@ -0,0 +1,16 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class BustedState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(-1); + + public BustedState(final Hand hand) { + super(hand); + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } +} diff --git a/src/main/java/domain/participant/hand/FinishedState.java b/src/main/java/domain/participant/hand/FinishedState.java new file mode 100644 index 00000000000..314e1cf6f3a --- /dev/null +++ b/src/main/java/domain/participant/hand/FinishedState.java @@ -0,0 +1,23 @@ +package domain.participant.hand; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public abstract class FinishedState extends StartedState { + public FinishedState(final Hand hand) { + super(hand); + } + + @Override + public BigDecimal calculateProfit(final BigDecimal money) { + return money.multiply(getProfitRate()) + .setScale(0, RoundingMode.HALF_UP); + } + + @Override + public boolean isFinished() { + return true; + } + + protected abstract BigDecimal getProfitRate(); +} diff --git a/src/main/java/domain/participant/hand/HandState.java b/src/main/java/domain/participant/hand/HandState.java new file mode 100644 index 00000000000..f7d60a58b9c --- /dev/null +++ b/src/main/java/domain/participant/hand/HandState.java @@ -0,0 +1,23 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +import domain.card.Card; + +public interface HandState { + default HandState draw(final Card card) { + throw new UnsupportedOperationException("hit을 할 수 없는 상태입니다."); + } + + default HandState stay() { + throw new UnsupportedOperationException("stay를 할 수 없는 상태입니다."); + } + + boolean isFinished(); + + Hand getHand(); + + default BigDecimal calculateProfit(final BigDecimal money) { + throw new UnsupportedOperationException("수익을 계산할 수 없는 상태입니다."); + } +} diff --git a/src/main/java/domain/participant/hand/HandStateFactory.java b/src/main/java/domain/participant/hand/HandStateFactory.java new file mode 100644 index 00000000000..2a4e96fbd07 --- /dev/null +++ b/src/main/java/domain/participant/hand/HandStateFactory.java @@ -0,0 +1,14 @@ +package domain.participant.hand; + +public class HandStateFactory { + private HandStateFactory() { + throw new AssertionError(); + } + + public static HandState createFromInitialHand(final Hand hand) { + if (hand.isBlackjack()) { + return new BlackjackState(hand); + } + return new HitState(hand); + } +} diff --git a/src/main/java/domain/participant/hand/HitState.java b/src/main/java/domain/participant/hand/HitState.java new file mode 100644 index 00000000000..fe3182f1b0c --- /dev/null +++ b/src/main/java/domain/participant/hand/HitState.java @@ -0,0 +1,28 @@ +package domain.participant.hand; + +import domain.card.Card; + +public class HitState extends StartedState { + public HitState(final Hand hand) { + super(hand); + } + + @Override + public HandState draw(final Card card) { + hand.draw(card); + if (hand.isBust()) { + return new BustedState(hand); + } + return new HitState(hand); + } + + @Override + public HandState stay() { + return new StayState(hand); + } + + @Override + public boolean isFinished() { + return false; + } +} diff --git a/src/main/java/domain/participant/hand/StartedState.java b/src/main/java/domain/participant/hand/StartedState.java new file mode 100644 index 00000000000..77abbbc71eb --- /dev/null +++ b/src/main/java/domain/participant/hand/StartedState.java @@ -0,0 +1,14 @@ +package domain.participant.hand; + +public abstract class StartedState implements HandState { + protected final Hand hand; + + public StartedState(final Hand hand) { + this.hand = hand; + } + + @Override + public Hand getHand() { + return hand; + } +} diff --git a/src/main/java/domain/participant/hand/StayState.java b/src/main/java/domain/participant/hand/StayState.java new file mode 100644 index 00000000000..32fb4566ec5 --- /dev/null +++ b/src/main/java/domain/participant/hand/StayState.java @@ -0,0 +1,16 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public class StayState extends FinishedState { + private static final BigDecimal PROFIT_RATE = BigDecimal.valueOf(1); + + public StayState(final Hand hand) { + super(hand); + } + + @Override + protected BigDecimal getProfitRate() { + return PROFIT_RATE; + } +} diff --git a/src/test/java/domain/BettingMoneyTest.java b/src/test/java/domain/BettingMoneyTest.java index 4b9da972bb8..945c355f142 100644 --- a/src/test/java/domain/BettingMoneyTest.java +++ b/src/test/java/domain/BettingMoneyTest.java @@ -1,5 +1,6 @@ package domain; +import static java.math.BigDecimal.valueOf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -9,14 +10,14 @@ import org.junit.jupiter.params.provider.ValueSource; class BettingMoneyTest { - @DisplayName("생성자: 금액을 입력받아 인스턴스 생성") + @DisplayName("constructor: 금액을 입력받아 인스턴스 생성") @ValueSource(ints = {100, 1_000, 5_000, 10_500}) @ParameterizedTest void constructor(final int amount) { assertThat(new BettingMoney(amount)).isInstanceOf(BettingMoney.class); } - @DisplayName("생성자: 최소 베팅 금액을 충족하지 못한 경우 예외 발생") + @DisplayName("constructor: 최소 베팅 금액을 충족하지 못한 경우 예외 발생") @ValueSource(ints = {0, 99}) @ParameterizedTest void constructor_LackOfAmount_ExceptionThrown(final int amount) { @@ -25,11 +26,19 @@ void constructor_LackOfAmount_ExceptionThrown(final int amount) { .hasMessageContaining("베팅 최소 금액을 충족하지 못했습니다"); } - @DisplayName("생성자: 금액의 단위가 못한 경우 예외 발생") + @DisplayName("constructor: 금액의 단위가 못한 경우 예외 발생") @Test void constructor_BettingUnitMismatch_ExceptionThrown() { assertThatThrownBy(() -> new BettingMoney(101)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("금액의 단위가 올바르지 않습니다"); } + + @DisplayName("getAmount: 금액을 반환") + @Test + void getAmount() { + BettingMoney bettingMoney = new BettingMoney(1_000); + + assertThat(bettingMoney.getAmount()).isEqualTo(valueOf(1_000)); + } } diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index 0096e64258c..97d2c860d63 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -15,6 +15,7 @@ import java.util.List; import domain.card.Card; +import domain.participant.hand.Hand; public class Fixture { public static final List CARDS = Card.values(); @@ -42,4 +43,12 @@ public class Fixture { public static final List MAX_SCORE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE); public static final List DEALER_HITTABLE_UPPER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SIX_SCORE); public static final List DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SEVEN_SCORE); + + public static Hand createHand(List cards) { + Hand hand = new Hand(); + for (final Card card : cards) { + hand.draw(card); + } + return hand; + } } diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java index d80216be858..d01cb4ee316 100644 --- a/src/test/java/domain/NameTest.java +++ b/src/test/java/domain/NameTest.java @@ -9,14 +9,14 @@ import org.junit.jupiter.params.provider.ValueSource; class NameTest { - @DisplayName("생성자: 1~5자 사이의 이름을 입력받아 인스턴스 생성") + @DisplayName("constructor: 1~5자 사이의 이름을 입력받아 인스턴스 생성") @ValueSource(strings = {"뭐", "hello", " hello ", " 다섯글자야", " hello"}) @ParameterizedTest void constructor(final String name) { assertThat(new Name(name)).isInstanceOf(Name.class); } - @DisplayName("생성자: 입력받은 이름이 null이면 예외 발생") + @DisplayName("constructor: 입력받은 이름이 null이면 예외 발생") @Test void constructor_NameIsNull_ExceptionThrown() { assertThatThrownBy(() -> new Name(null)) @@ -24,7 +24,7 @@ void constructor_NameIsNull_ExceptionThrown() { .hasMessageContaining("이름이 null입니다"); } - @DisplayName("생성자: 길이가 올바르지 않은 이름을 입력받아 예외 발생") + @DisplayName("constructor: 길이가 올바르지 않은 이름을 입력받아 예외 발생") @ValueSource(strings = {"", " ", " ", "다섯자넘어유~"}) @ParameterizedTest void constructor_InvalidNameLength_ExceptionThrown(final String name) { @@ -38,4 +38,13 @@ void constructor_InvalidNameLength_ExceptionThrown(final String name) { void fromComma() { assertThat(Name.fromComma("a, hell, hello, 메롱, the, he")).hasSize(6); } + + @DisplayName("getName: 이름을 반환") + @ValueSource(strings = {" hell", "hell ", " hell "}) + @ParameterizedTest + void getName(final String nameWithSpace) { + Name name = new Name(nameWithSpace); + + assertThat(name.getName()).isEqualTo("hell"); + } } diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java index 89e9982c630..edc295d840c 100644 --- a/src/test/java/domain/card/CardTest.java +++ b/src/test/java/domain/card/CardTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -public class CardTest { +class CardTest { @DisplayName("fromFaceAndSuit: 카드 한 장을 생성") @Test void fromFaceAndSuit() { diff --git a/src/test/java/domain/card/RandomCardDeckTest.java b/src/test/java/domain/card/RandomCardDeckTest.java index d3ce8dd5635..4c834fb050f 100644 --- a/src/test/java/domain/card/RandomCardDeckTest.java +++ b/src/test/java/domain/card/RandomCardDeckTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class RandomCardDeckTest { +class RandomCardDeckTest { private RandomCardDeck cardDeck; @BeforeEach @@ -18,14 +18,14 @@ void setUp() { cardDeck = new RandomCardDeck(CARDS); } - @DisplayName("생성자: 카드 덱 생성") + @DisplayName("constructor: 카드 덱 생성") @Test void constructor() { assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); } - @DisplayName("생성자: 카드 리스트가 null인 경우 예외 발생") + @DisplayName("constructor: 카드 리스트가 null인 경우 예외 발생") @Test void constructor_CardsIsNull_ExceptionThrown() { assertThatThrownBy(() -> new RandomCardDeck(null)) @@ -33,7 +33,7 @@ void constructor_CardsIsNull_ExceptionThrown() { .hasMessageContaining("카드 리스트가 null입니다"); } - @DisplayName("생성자: 카드 리스트의 크기가 0인 경우 예외 발생") + @DisplayName("constructor: 카드 리스트의 크기가 0인 경우 예외 발생") @Test void constructor_CardsIsEmpty_ExceptionThrown() { assertThatThrownBy(() -> new RandomCardDeck(new ArrayList<>())) diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java new file mode 100644 index 00000000000..102b1d3bf1f --- /dev/null +++ b/src/test/java/domain/participant/DealerTest.java @@ -0,0 +1,14 @@ +package domain.participant; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DealerTest { + @DisplayName("constructor: 딜러 인스턴스 생성") + @Test + void constructor() { + assertThat(new Dealer()).isInstanceOf(Dealer.class); + } +} diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java index 281d48f2fc4..24543412eb2 100644 --- a/src/test/java/domain/participant/PlayerTest.java +++ b/src/test/java/domain/participant/PlayerTest.java @@ -2,20 +2,20 @@ import static domain.Fixture.HUNDRED_BETTING_MONEY; import static domain.Fixture.JUN; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class PlayerTest { - @DisplayName("생성자: 사용자 생성") +class PlayerTest { + @DisplayName("constructor: 사용자 생성") @Test void constructor() { - Assertions.assertThat(new Player(JUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + assertThat(new Player(JUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); } - @DisplayName("생성자: Name이 null이면 예외 발생") + @DisplayName("constructor: Name이 null이면 예외 발생") @Test void constructor_NameIsNull_ExceptionThrown() { assertThatThrownBy(() -> new Player(null, HUNDRED_BETTING_MONEY)) @@ -23,11 +23,27 @@ void constructor_NameIsNull_ExceptionThrown() { .hasMessageContaining("name이 null입니다"); } - @DisplayName("생성자: BettingMoney가 null이면 예외 발생") + @DisplayName("constructor: BettingMoney가 null이면 예외 발생") @Test void constructor_BettingMoneyIsNull_ExceptionThrown() { assertThatThrownBy(() -> new Player(JUN, null)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("bettingMoney가 null입니다"); } + + @DisplayName("getName: 이름을 반환") + @Test + void getName() { + Player player = new Player(JUN, HUNDRED_BETTING_MONEY); + + assertThat(player.getName()).isEqualTo(JUN); + } + + @DisplayName("getBettingMoney: 베팅 금액을 반환") + @Test + void getBettingMoney() { + Player player = new Player(JUN, HUNDRED_BETTING_MONEY); + + assertThat(player.getBettingMoney()).isEqualTo(HUNDRED_BETTING_MONEY); + } } diff --git a/src/test/java/domain/participant/hand/BlackjackStateTest.java b/src/test/java/domain/participant/hand/BlackjackStateTest.java new file mode 100644 index 00000000000..862e1969ca1 --- /dev/null +++ b/src/test/java/domain/participant/hand/BlackjackStateTest.java @@ -0,0 +1,48 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BlackjackStateTest { + private static final BlackjackState BLACKJACK_STATE = new BlackjackState(createHand(BLACKJACK_CARDS)); + + @DisplayName("constructor: blackjack 상태 생성") + @Test + void constructor() { + assertThat(new BlackjackState(createHand(BLACKJACK_CARDS))) + .isInstanceOf(BlackjackState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> BLACKJACK_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(BLACKJACK_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(BLACKJACK_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateProfit() { + assertThat(BLACKJACK_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(1_500)); + } +} diff --git a/src/test/java/domain/participant/hand/BustedStateTest.java b/src/test/java/domain/participant/hand/BustedStateTest.java new file mode 100644 index 00000000000..8f96afae587 --- /dev/null +++ b/src/test/java/domain/participant/hand/BustedStateTest.java @@ -0,0 +1,49 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.BUSTED_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BustedStateTest { + private static final BustedState BUSTED_STATE = new BustedState(createHand(BUSTED_CARDS)); + + @DisplayName("constructor: busted 상태 생성") + @Test + void constructor() { + assertThat(new BustedState(createHand(BLACKJACK_CARDS))) + .isInstanceOf(BustedState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> BUSTED_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(BUSTED_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(BUSTED_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateProfit() { + assertThat(BUSTED_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(-1_000)); + } +} diff --git a/src/test/java/domain/participant/hand/HandStateFactoryTest.java b/src/test/java/domain/participant/hand/HandStateFactoryTest.java new file mode 100644 index 00000000000..e828525692d --- /dev/null +++ b/src/test/java/domain/participant/hand/HandStateFactoryTest.java @@ -0,0 +1,29 @@ +package domain.participant.hand; + +import static domain.Fixture.BLACKJACK_CARDS; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; + +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 HandStateFactoryTest { + private static Stream createCardsAndExpectResult() { + return Stream.of( + Arguments.of(createHand(BLACKJACK_CARDS), BlackjackState.class), + Arguments.of(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS), HitState.class) + ); + } + + @DisplayName("createFromInitialHand: 카드를 입력받아 초기 상태 생성") + @MethodSource("createCardsAndExpectResult") + @ParameterizedTest + void createFromInitialHand(final Hand hand, final Class expect) { + assertThat(HandStateFactory.createFromInitialHand(hand)).isInstanceOf(expect); + } +} \ No newline at end of file diff --git a/src/test/java/domain/participant/hand/HandTest.java b/src/test/java/domain/participant/hand/HandTest.java index f04bf117601..2bfb331e2d6 100644 --- a/src/test/java/domain/participant/hand/HandTest.java +++ b/src/test/java/domain/participant/hand/HandTest.java @@ -21,7 +21,7 @@ import domain.card.Card; -public class HandTest { +class HandTest { private static Stream createCardsAndScore() { return Stream.of( Arguments.of(BUSTED_CARDS, 22), @@ -51,7 +51,7 @@ private static Stream createCardsAndIsBust() { ); } - @DisplayName("생성자: Hand 인스턴스 생성") + @DisplayName("constructor: Hand 인스턴스 생성") @Test void constructor() { assertThat(new Hand()).isInstanceOf(Hand.class); diff --git a/src/test/java/domain/participant/hand/HitStateTest.java b/src/test/java/domain/participant/hand/HitStateTest.java new file mode 100644 index 00000000000..efd62f0a59d --- /dev/null +++ b/src/test/java/domain/participant/hand/HitStateTest.java @@ -0,0 +1,70 @@ +package domain.participant.hand; + +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.FOUR_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; +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; + +import domain.card.Card; + +class HitStateTest { + private static final HitState HIT_STATE = new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + + private static Stream createCardAndIsNextBust() { + return Stream.of( + Arguments.of(FOUR_SCORE, HitState.class), + Arguments.of(ACE_SCORE, HitState.class), + Arguments.of(FOUR_SCORE, BustedState.class) + ); + } + + @DisplayName("constructor: 초기 카드로 HitState 생성") + @Test + void constructor() { + assertThat(new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS))) + .isInstanceOf(HitState.class); + } + + @DisplayName("draw: 카드를 한 장 받아 다음 상태로 전이") + @MethodSource("createCardAndIsNextBust") + @ParameterizedTest + void draw(final Card card, final Class expect) { + assertThat(HIT_STATE.draw(card)).isInstanceOf(expect); + } + + @DisplayName("isFinished: 끝나지 않았음을 반환") + @Test + void isFinished() { + assertThat(HIT_STATE.isFinished()).isFalse(); + } + + @DisplayName("stay: 턴을 마치고 상태 전이") + @Test + void stay() { + assertThat(HIT_STATE.stay()).isInstanceOf(StayState.class); + } + + @DisplayName("calculateHand: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void calculateHand() { + assertThatThrownBy(() -> HIT_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("getHand: 가지고 있는 패를 반환") + @Test + void getHand() { + assertThat(HIT_STATE.getHand()).isInstanceOf(Hand.class); + } +} diff --git a/src/test/java/domain/participant/hand/StayStateTest.java b/src/test/java/domain/participant/hand/StayStateTest.java new file mode 100644 index 00000000000..ebc81dd149b --- /dev/null +++ b/src/test/java/domain/participant/hand/StayStateTest.java @@ -0,0 +1,48 @@ +package domain.participant.hand; + +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.createHand; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StayStateTest { + private static final StayState STAY_STATE = new StayState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + + @DisplayName("constructor: stay 상태 생성") + @Test + void constructor() { + assertThat(new StayState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS))) + .isInstanceOf(StayState.class); + } + + @DisplayName("draw: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void draw() { + assertThatThrownBy(() -> STAY_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("isFinished: 끝났음을 반환") + @Test + void isFinished() { + assertThat(STAY_STATE.isFinished()).isTrue(); + } + + @DisplayName("stay: 지원하지 않는 메서드를 호출하여 예외 발생") + @Test + void stay() { + assertThatThrownBy(STAY_STATE::stay).isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName("calculateProfit: 수익금 계산") + @Test + void calculateHand() { + assertThat(STAY_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + .isEqualTo(BigDecimal.valueOf(1_000)); + } +} From 6455acaa9e83e7b8bb0f25919b75ee516b30f02e Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Wed, 1 Jul 2020 18:41:17 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20Dealer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/participant/Dealer.java | 22 ++++++ .../java/domain/participant/Participant.java | 37 +++++++++ src/main/java/domain/participant/Player.java | 13 +++- .../domain/participant/hand/HandState.java | 8 +- .../domain/participant/hand/StartedState.java | 5 ++ src/test/java/domain/Fixture.java | 13 ++++ .../java/domain/participant/DealerTest.java | 29 ++++++- .../java/domain/participant/PlayerTest.java | 77 ++++++++++++++++--- 8 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/main/java/domain/participant/Dealer.java create mode 100644 src/main/java/domain/participant/Participant.java diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java new file mode 100644 index 00000000000..11dafff7a11 --- /dev/null +++ b/src/main/java/domain/participant/Dealer.java @@ -0,0 +1,22 @@ +package domain.participant; + +import domain.Name; +import domain.card.Card; +import domain.participant.hand.HandState; + +public class Dealer extends Participant { + private static final Name NAME = new Name("딜러"); + private static final int DEALER_HITTABLE_UPPER_BOUND = 16; + + public Dealer(final HandState hand) { + super(NAME, hand); + } + + @Override + public void hit(final Card card) { + super.hit(card); + if (hand.isOver(DEALER_HITTABLE_UPPER_BOUND)) { + stay(); + } + } +} diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 00000000000..f0fc8fec8d6 --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,37 @@ +package domain.participant; + +import java.util.Objects; + +import domain.Name; +import domain.card.Card; +import domain.participant.hand.HandState; + +public abstract class Participant { + private final Name name; + HandState hand; + + public Participant(final Name name, final HandState hand) { + this.name = Objects.requireNonNull(name, "name이 null입니다."); + this.hand = Objects.requireNonNull(hand, "handState가 null입니다."); + } + + public void hit(final Card card) { + hand = hand.draw(card); + } + + public void stay() { + hand = hand.stay(); + } + + public boolean isFinished() { + return hand.isFinished(); + } + + public Name getName() { + return name; + } + + public HandState getHandState() { + return hand; + } +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java index 67fcbb87727..e62ac82f56c 100644 --- a/src/main/java/domain/participant/Player.java +++ b/src/main/java/domain/participant/Player.java @@ -1,16 +1,21 @@ package domain.participant; +import java.math.BigDecimal; import java.util.Objects; import domain.BettingMoney; import domain.Name; +import domain.participant.hand.HandState; -public class Player { - private final Name name; +public class Player extends Participant { private final BettingMoney bettingMoney; - public Player(final Name name, final BettingMoney bettingMoney) { - this.name = Objects.requireNonNull(name, "name이 null입니다."); + public Player(final Name name, final HandState handState, final BettingMoney bettingMoney) { + super(name, handState); this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); } + + public BigDecimal calculateProfit() { + return hand.calculateProfit(bettingMoney.getAmount()); + } } diff --git a/src/main/java/domain/participant/hand/HandState.java b/src/main/java/domain/participant/hand/HandState.java index f7d60a58b9c..16e785a14f8 100644 --- a/src/main/java/domain/participant/hand/HandState.java +++ b/src/main/java/domain/participant/hand/HandState.java @@ -15,9 +15,15 @@ default HandState stay() { boolean isFinished(); - Hand getHand(); + default boolean isOver(final int score) { + return calculateScore() > score; + } + + int calculateScore(); default BigDecimal calculateProfit(final BigDecimal money) { throw new UnsupportedOperationException("수익을 계산할 수 없는 상태입니다."); } + + Hand getHand(); } diff --git a/src/main/java/domain/participant/hand/StartedState.java b/src/main/java/domain/participant/hand/StartedState.java index 77abbbc71eb..fd2586d100b 100644 --- a/src/main/java/domain/participant/hand/StartedState.java +++ b/src/main/java/domain/participant/hand/StartedState.java @@ -7,6 +7,11 @@ public StartedState(final Hand hand) { this.hand = hand; } + @Override + public int calculateScore() { + return hand.calculateScore(); + } + @Override public Hand getHand() { return hand; diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index 97d2c860d63..a1910506d6b 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -16,6 +16,8 @@ import domain.card.Card; import domain.participant.hand.Hand; +import domain.participant.hand.HandState; +import domain.participant.hand.HandStateFactory; public class Fixture { public static final List CARDS = Card.values(); @@ -41,9 +43,16 @@ public class Fixture { public static final List BUSTED_BY_ACE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE, ACE_SCORE); public static final List BLACKJACK_CARDS = Arrays.asList(TEN_SCORE, ACE_SCORE); public static final List MAX_SCORE_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, ACE_SCORE); + public static final List DEALER_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TWO_SCORE, TWO_SCORE); public static final List DEALER_HITTABLE_UPPER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SIX_SCORE); public static final List DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SEVEN_SCORE); + public static final Hand HITTABLE_HAND = createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS); + public static final Hand BLACKJACK_HAND = createHand(BLACKJACK_CARDS); + + public static final HandState HITTABLE_HAND_STATE = HandStateFactory.createFromInitialHand(HITTABLE_HAND); + public static final HandState BLACKJACK_HAND_STATE = HandStateFactory.createFromInitialHand(BLACKJACK_HAND); + public static Hand createHand(List cards) { Hand hand = new Hand(); for (final Card card : cards) { @@ -51,4 +60,8 @@ public static Hand createHand(List cards) { } return hand; } + + public static HandState createHandState(List cards) { + return HandStateFactory.createFromInitialHand(createHand(cards)); + } } diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java index 102b1d3bf1f..e66ec55eaff 100644 --- a/src/test/java/domain/participant/DealerTest.java +++ b/src/test/java/domain/participant/DealerTest.java @@ -1,14 +1,41 @@ package domain.participant; +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.DEALER_HITTABLE_LOWER_BOUND_CARDS; +import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.createHandState; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import domain.participant.hand.HitState; +import domain.participant.hand.StayState; + class DealerTest { @DisplayName("constructor: 딜러 인스턴스 생성") @Test void constructor() { - assertThat(new Dealer()).isInstanceOf(Dealer.class); + assertThat(new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS))).isInstanceOf(Dealer.class); + } + + @DisplayName("hit: 카드 한 장을 받고 16 이하이면 Hittable 상태 유지") + @Test + void hit_HittableState() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + + dealer.hit(ACE_SCORE); + + assertThat(dealer.getHandState()).isInstanceOf(HitState.class); + } + + @DisplayName("hit: 카드 한 장을 받고 16 초과하면 stay 상태로 전이") + @Test + void hit_ChangeToStayState() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + + dealer.hit(ACE_SCORE); + + assertThat(dealer.getHandState()).isInstanceOf(StayState.class); } } diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java index 24543412eb2..bfe77eb0f3f 100644 --- a/src/test/java/domain/participant/PlayerTest.java +++ b/src/test/java/domain/participant/PlayerTest.java @@ -1,49 +1,104 @@ package domain.participant; +import static domain.Fixture.HITTABLE_HAND_STATE; import static domain.Fixture.HUNDRED_BETTING_MONEY; import static domain.Fixture.JUN; +import static domain.Fixture.POBI; +import static domain.Fixture.TEN_SCORE; +import static domain.Fixture.TWO_SCORE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import java.math.BigDecimal; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import domain.participant.hand.BustedState; +import domain.participant.hand.HitState; +import domain.participant.hand.StayState; + class PlayerTest { + private Player player; + + @BeforeEach + void setUp() { + player = new Player(JUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY); + } + @DisplayName("constructor: 사용자 생성") @Test void constructor() { - assertThat(new Player(JUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + assertThat(new Player(JUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); } @DisplayName("constructor: Name이 null이면 예외 발생") @Test void constructor_NameIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Player(null, HUNDRED_BETTING_MONEY)) + assertThatThrownBy(() -> new Player(null, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("name이 null입니다"); } + @DisplayName("constructor: HandState가 null이면 예외 발생") + @Test + void constructor_HandStateIsNull_ExceptionThrown() { + assertThatThrownBy(() -> new Player(POBI, null, HUNDRED_BETTING_MONEY)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("handState가 null입니다"); + } + @DisplayName("constructor: BettingMoney가 null이면 예외 발생") @Test void constructor_BettingMoneyIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Player(JUN, null)) + assertThatThrownBy(() -> new Player(JUN, HITTABLE_HAND_STATE, null)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("bettingMoney가 null입니다"); } - @DisplayName("getName: 이름을 반환") + @DisplayName("hit: 카드를 한 장 뽑고 상태 전이") @Test - void getName() { - Player player = new Player(JUN, HUNDRED_BETTING_MONEY); + void hit() { + player.hit(TWO_SCORE); - assertThat(player.getName()).isEqualTo(JUN); + assertThat(player.getHandState()).isInstanceOf(HitState.class); + assertThat(player.isFinished()).isFalse(); } - @DisplayName("getBettingMoney: 베팅 금액을 반환") + @DisplayName("hit: 카드를 한 장 뽑고 버스트 상태로 전이") @Test - void getBettingMoney() { - Player player = new Player(JUN, HUNDRED_BETTING_MONEY); + void hit_Busted() { + player.hit(TEN_SCORE); - assertThat(player.getBettingMoney()).isEqualTo(HUNDRED_BETTING_MONEY); + assertAll( + () -> assertThat(player.getHandState()).isInstanceOf(BustedState.class), + () -> assertThat(player.isFinished()).isTrue() + ); + } + + @DisplayName("stay: 카드를 한 장 뽑고 상태 전이") + @Test + void stay() { + player.stay(); + + assertAll( + () -> assertThat(player.getHandState()).isInstanceOf(StayState.class), + () -> assertThat(player.isFinished()).isTrue() + ); + } + + @DisplayName("수익률 계산") + @Test + void calculateProfit() { + player.stay(); + assertThat(player.calculateProfit()).isEqualTo(BigDecimal.valueOf(1_000_000)); + } + + @DisplayName("getName: 이름을 반환") + @Test + void getName() { + assertThat(player.getName()).isEqualTo(JUN); } } From cf4a75c7729c2d4c1eca3f0019385ab49dafddcd Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Wed, 1 Jul 2020 22:49:59 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20GameResult=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/main/java/domain/Name.java | 2 +- .../java/domain/participant/Participant.java | 25 ++++++ .../participant/hand/BlackjackState.java | 5 ++ .../domain/participant/hand/BustedState.java | 5 ++ .../participant/hand/FinishedState.java | 8 ++ src/main/java/domain/result/DrawStrategy.java | 21 +++++ src/main/java/domain/result/GameResult.java | 29 ++++++ .../domain/result/PlayerLoseStrategy.java | 21 +++++ .../java/domain/result/PlayerWinStrategy.java | 21 +++++ .../domain/result/ResultDecideStrategy.java | 22 +++++ src/test/java/domain/Fixture.java | 12 ++- src/test/java/domain/NameTest.java | 4 +- .../java/domain/participant/PlayerTest.java | 14 +-- .../java/domain/result/GameResultTest.java | 89 +++++++++++++++++++ 15 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 src/main/java/domain/result/DrawStrategy.java create mode 100644 src/main/java/domain/result/GameResult.java create mode 100644 src/main/java/domain/result/PlayerLoseStrategy.java create mode 100644 src/main/java/domain/result/PlayerWinStrategy.java create mode 100644 src/main/java/domain/result/ResultDecideStrategy.java create mode 100644 src/test/java/domain/result/GameResultTest.java diff --git a/README.md b/README.md index 82168f5ac9c..db306dca80e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## 기능 목록 * [X] 게임에 참여할 사람의 이름을 입력받는다. - * [X] 참여자의 이름은 1 ~ 5자로 구성된다. + * [X] 참여자의 이름은 1 ~ 10자로 구성된다. * [X] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. * [X] 참여자의 이름은 comma(,) 단위로 구분한다. * [ ] 참여자의 수는 2 ~ 8명이다. diff --git a/src/main/java/domain/Name.java b/src/main/java/domain/Name.java index 4a29216d044..d9ee5a6750e 100644 --- a/src/main/java/domain/Name.java +++ b/src/main/java/domain/Name.java @@ -7,7 +7,7 @@ import java.util.stream.Stream; public class Name { - private static final int MAX_LENGTH = 5; + private static final int MAX_LENGTH = 10; private static final int MIN_LENGTH = 1; private static final String NAME_DELIMITER = ","; diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java index f0fc8fec8d6..ec193af3911 100644 --- a/src/main/java/domain/participant/Participant.java +++ b/src/main/java/domain/participant/Participant.java @@ -4,6 +4,7 @@ import domain.Name; import domain.card.Card; +import domain.participant.hand.FinishedState; import domain.participant.hand.HandState; public abstract class Participant { @@ -27,6 +28,30 @@ public boolean isFinished() { return hand.isFinished(); } + public boolean isBusted() { + if (!isFinished()) { + return false; + } + FinishedState state = (FinishedState)hand; + return state.isBusted(); + } + + public boolean isBlackjack() { + if (!isFinished()) { + return false; + } + FinishedState state = (FinishedState)hand; + return state.isBlackjack(); + } + + public int compareScore(final Participant that) { + return Integer.compare(calculateScore(), that.calculateScore()); + } + + private int calculateScore() { + return hand.calculateScore(); + } + public Name getName() { return name; } diff --git a/src/main/java/domain/participant/hand/BlackjackState.java b/src/main/java/domain/participant/hand/BlackjackState.java index 43910b95685..e18ab882968 100644 --- a/src/main/java/domain/participant/hand/BlackjackState.java +++ b/src/main/java/domain/participant/hand/BlackjackState.java @@ -9,6 +9,11 @@ public BlackjackState(final Hand hand) { super(hand); } + @Override + public boolean isBlackjack() { + return true; + } + @Override protected BigDecimal getProfitRate() { return PROFIT_RATE; diff --git a/src/main/java/domain/participant/hand/BustedState.java b/src/main/java/domain/participant/hand/BustedState.java index 85a2c145d85..c541420aed1 100644 --- a/src/main/java/domain/participant/hand/BustedState.java +++ b/src/main/java/domain/participant/hand/BustedState.java @@ -9,6 +9,11 @@ public BustedState(final Hand hand) { super(hand); } + @Override + public boolean isBusted() { + return true; + } + @Override protected BigDecimal getProfitRate() { return PROFIT_RATE; diff --git a/src/main/java/domain/participant/hand/FinishedState.java b/src/main/java/domain/participant/hand/FinishedState.java index 314e1cf6f3a..98b6f613c93 100644 --- a/src/main/java/domain/participant/hand/FinishedState.java +++ b/src/main/java/domain/participant/hand/FinishedState.java @@ -19,5 +19,13 @@ public boolean isFinished() { return true; } + public boolean isBlackjack() { + return false; + } + + public boolean isBusted() { + return false; + } + protected abstract BigDecimal getProfitRate(); } diff --git a/src/main/java/domain/result/DrawStrategy.java b/src/main/java/domain/result/DrawStrategy.java new file mode 100644 index 00000000000..53c3770eba7 --- /dev/null +++ b/src/main/java/domain/result/DrawStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class DrawStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return dealer.isBlackjack() && player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return false; + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) == 0; + } +} diff --git a/src/main/java/domain/result/GameResult.java b/src/main/java/domain/result/GameResult.java new file mode 100644 index 00000000000..d7e8b1f7eee --- /dev/null +++ b/src/main/java/domain/result/GameResult.java @@ -0,0 +1,29 @@ +package domain.result; + +import java.util.stream.Stream; + +import domain.participant.Dealer; +import domain.participant.Player; + +public enum GameResult { + PLAYER_WIN(new PlayerWinStrategy()), + DRAW(new DrawStrategy()), + PLAYER_LOSE(new PlayerLoseStrategy()); + + private final ResultDecideStrategy resultDecideStrategy; + + GameResult(final ResultDecideStrategy resultDecideStrategy) { + this.resultDecideStrategy = resultDecideStrategy; + } + + public static GameResult fromDealerAndPlayer(final Dealer dealer, final Player player) { + return Stream.of(values()) + .filter(result -> result.matches(dealer, player)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("결과가 존재하지 않습니다.")); + } + + private boolean matches(final Dealer dealer, final Player player) { + return resultDecideStrategy.matches(dealer, player); + } +} \ No newline at end of file diff --git a/src/main/java/domain/result/PlayerLoseStrategy.java b/src/main/java/domain/result/PlayerLoseStrategy.java new file mode 100644 index 00000000000..373b40c82d6 --- /dev/null +++ b/src/main/java/domain/result/PlayerLoseStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class PlayerLoseStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return dealer.isBlackjack() && !player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return player.isBusted(); + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) > 0; + } +} diff --git a/src/main/java/domain/result/PlayerWinStrategy.java b/src/main/java/domain/result/PlayerWinStrategy.java new file mode 100644 index 00000000000..53240753ff7 --- /dev/null +++ b/src/main/java/domain/result/PlayerWinStrategy.java @@ -0,0 +1,21 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public class PlayerWinStrategy implements ResultDecideStrategy { + @Override + public boolean matchByBlackjack(final Dealer dealer, final Player player) { + return !dealer.isBlackjack() && player.isBlackjack(); + } + + @Override + public boolean matchByBust(final Dealer dealer, final Player player) { + return dealer.isBusted() && !player.isBusted(); + } + + @Override + public boolean matchByScore(final Dealer dealer, final Player player) { + return dealer.compareScore(player) < 0; + } +} diff --git a/src/main/java/domain/result/ResultDecideStrategy.java b/src/main/java/domain/result/ResultDecideStrategy.java new file mode 100644 index 00000000000..328af3f65ae --- /dev/null +++ b/src/main/java/domain/result/ResultDecideStrategy.java @@ -0,0 +1,22 @@ +package domain.result; + +import domain.participant.Dealer; +import domain.participant.Player; + +public interface ResultDecideStrategy { + default boolean matches(final Dealer dealer, final Player player) { + if (dealer.isBlackjack() || player.isBlackjack()) { + return matchByBlackjack(dealer, player); + } + if (dealer.isBusted() || player.isBusted()) { + return matchByBust(dealer, player); + } + return matchByScore(dealer, player); + } + + boolean matchByBlackjack(final Dealer dealer, final Player player); + + boolean matchByBust(final Dealer dealer, final Player player); + + boolean matchByScore(final Dealer dealer, final Player player); +} diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index a1910506d6b..f77dd6eeca4 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -22,8 +22,10 @@ public class Fixture { public static final List CARDS = Card.values(); - public static final Name POBI = new Name("Pobi"); - public static final Name JUN = new Name("Jun"); + public static final Name SOUTHJUN = new Name("southjun"); + public static final Name NORTHJUN = new Name("northjun"); + public static final Name WESTJUN = new Name("westjun"); + public static final Name EASTJUN = new Name("eastjun"); public static final BettingMoney THOUSAND_BETTING_MONEY = new BettingMoney(1_000); public static final BettingMoney HUNDRED_BETTING_MONEY = new BettingMoney(1_000_000); @@ -47,10 +49,12 @@ public class Fixture { public static final List DEALER_HITTABLE_UPPER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SIX_SCORE); public static final List DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS = Arrays.asList(TEN_SCORE, SEVEN_SCORE); - public static final Hand HITTABLE_HAND = createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS); + public static final Hand HITTABLE_LOW_HAND = createHand(DEALER_HITTABLE_LOWER_BOUND_CARDS); + public static final Hand HITTABLE_HIGH_HAND = createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS); public static final Hand BLACKJACK_HAND = createHand(BLACKJACK_CARDS); + public static final Hand BUSTED_HAND = createHand(BUSTED_CARDS); - public static final HandState HITTABLE_HAND_STATE = HandStateFactory.createFromInitialHand(HITTABLE_HAND); + public static final HandState HITTABLE_HAND_STATE = HandStateFactory.createFromInitialHand(HITTABLE_HIGH_HAND); public static final HandState BLACKJACK_HAND_STATE = HandStateFactory.createFromInitialHand(BLACKJACK_HAND); public static Hand createHand(List cards) { diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java index d01cb4ee316..b9ce58d4a4a 100644 --- a/src/test/java/domain/NameTest.java +++ b/src/test/java/domain/NameTest.java @@ -10,7 +10,7 @@ class NameTest { @DisplayName("constructor: 1~5자 사이의 이름을 입력받아 인스턴스 생성") - @ValueSource(strings = {"뭐", "hello", " hello ", " 다섯글자야", " hello"}) + @ValueSource(strings = {"뭐", "hello", " hello ", " 오더하기오는십이예요", " hello"}) @ParameterizedTest void constructor(final String name) { assertThat(new Name(name)).isInstanceOf(Name.class); @@ -25,7 +25,7 @@ void constructor_NameIsNull_ExceptionThrown() { } @DisplayName("constructor: 길이가 올바르지 않은 이름을 입력받아 예외 발생") - @ValueSource(strings = {"", " ", " ", "다섯자넘어유~"}) + @ValueSource(strings = {"", " ", " ", "열한자는허락할수없어요"}) @ParameterizedTest void constructor_InvalidNameLength_ExceptionThrown(final String name) { assertThatThrownBy(() -> new Name(name)) diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java index bfe77eb0f3f..61b209beafc 100644 --- a/src/test/java/domain/participant/PlayerTest.java +++ b/src/test/java/domain/participant/PlayerTest.java @@ -1,11 +1,11 @@ package domain.participant; +import static domain.Fixture.EASTJUN; import static domain.Fixture.HITTABLE_HAND_STATE; import static domain.Fixture.HUNDRED_BETTING_MONEY; -import static domain.Fixture.JUN; -import static domain.Fixture.POBI; import static domain.Fixture.TEN_SCORE; import static domain.Fixture.TWO_SCORE; +import static domain.Fixture.WESTJUN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -25,13 +25,13 @@ class PlayerTest { @BeforeEach void setUp() { - player = new Player(JUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY); + player = new Player(EASTJUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY); } @DisplayName("constructor: 사용자 생성") @Test void constructor() { - assertThat(new Player(JUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + assertThat(new Player(EASTJUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); } @DisplayName("constructor: Name이 null이면 예외 발생") @@ -45,7 +45,7 @@ void constructor_NameIsNull_ExceptionThrown() { @DisplayName("constructor: HandState가 null이면 예외 발생") @Test void constructor_HandStateIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Player(POBI, null, HUNDRED_BETTING_MONEY)) + assertThatThrownBy(() -> new Player(WESTJUN, null, HUNDRED_BETTING_MONEY)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("handState가 null입니다"); } @@ -53,7 +53,7 @@ void constructor_HandStateIsNull_ExceptionThrown() { @DisplayName("constructor: BettingMoney가 null이면 예외 발생") @Test void constructor_BettingMoneyIsNull_ExceptionThrown() { - assertThatThrownBy(() -> new Player(JUN, HITTABLE_HAND_STATE, null)) + assertThatThrownBy(() -> new Player(EASTJUN, HITTABLE_HAND_STATE, null)) .isInstanceOf(NullPointerException.class) .hasMessageContaining("bettingMoney가 null입니다"); } @@ -99,6 +99,6 @@ void calculateProfit() { @DisplayName("getName: 이름을 반환") @Test void getName() { - assertThat(player.getName()).isEqualTo(JUN); + assertThat(player.getName()).isEqualTo(EASTJUN); } } diff --git a/src/test/java/domain/result/GameResultTest.java b/src/test/java/domain/result/GameResultTest.java new file mode 100644 index 00000000000..aa0a0788faa --- /dev/null +++ b/src/test/java/domain/result/GameResultTest.java @@ -0,0 +1,89 @@ +package domain.result; + +import static domain.Fixture.BLACKJACK_HAND; +import static domain.Fixture.BUSTED_HAND; +import static domain.Fixture.EASTJUN; +import static domain.Fixture.HITTABLE_HIGH_HAND; +import static domain.Fixture.HITTABLE_LOW_HAND; +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.NORTHJUN; +import static domain.Fixture.SOUTHJUN; +import static domain.Fixture.WESTJUN; +import static domain.result.GameResult.DRAW; +import static domain.result.GameResult.PLAYER_LOSE; +import static domain.result.GameResult.PLAYER_WIN; +import static domain.result.GameResult.fromDealerAndPlayer; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.participant.Dealer; +import domain.participant.Player; +import domain.participant.hand.BlackjackState; +import domain.participant.hand.BustedState; +import domain.participant.hand.StayState; + +public class GameResultTest { + private static final Dealer BLACKJACK_DEALER = new Dealer(new BlackjackState(BLACKJACK_HAND)); + private static final Dealer HIGH_SCORE_DEALER = new Dealer(new StayState(HITTABLE_HIGH_HAND)); + private static final Dealer LOW_SCORE_DEALER = new Dealer(new StayState(HITTABLE_LOW_HAND)); + private static final Dealer BUSTED_DEALER = new Dealer(new BustedState(BUSTED_HAND)); + + private static final Player BLACKJACK_PLAYER = + new Player(WESTJUN, new BlackjackState(BLACKJACK_HAND), HUNDRED_BETTING_MONEY); + private static final Player HIGH_SCORE_PLAYER = + new Player(EASTJUN, new StayState(HITTABLE_HIGH_HAND), HUNDRED_BETTING_MONEY); + private static final Player LOW_SCORE_PLAYER = + new Player(SOUTHJUN, new StayState(HITTABLE_LOW_HAND), HUNDRED_BETTING_MONEY); + private static final Player BUSTED_PLAYER = + new Player(NORTHJUN, new BustedState(BUSTED_HAND), HUNDRED_BETTING_MONEY); + + @DisplayName("플레이어가 블랙잭 승리") + @Test + void fromDealerAndPlayer_PlayerBlackjackWin() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, BLACKJACK_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("블랙잭 무승부") + @Test + void fromDealerAndPlayer_BlackjackDraw() { + assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, BLACKJACK_PLAYER)).isEqualTo(DRAW); + } + + @DisplayName("딜러가 블랙잭 승리") + @Test + void fromDealerAndPlayer_DealerBlackjackWin() { + assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); + } + + @DisplayName("플레이어가 버스트 패배") + @Test + void fromDealerAndPlayer_PlayerBustLose() { + assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, BUSTED_PLAYER)).isEqualTo(PLAYER_LOSE); + } + + @DisplayName("딜러가 버스트 패배") + @Test + void fromDealerAndPlayer_DealerBustLose() { + assertThat(fromDealerAndPlayer(BUSTED_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("플레이어가 점수를 비교하여 승리") + @Test + void fromDealerAndPlayer_PlayerScoreWin() { + assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); + } + + @DisplayName("점수를 비교하여 무승부") + @Test + void fromDealerAndPlayer_ScoreDraw() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(DRAW); + } + + @DisplayName("딜러가 점수를 비교하여 승리") + @Test + void fromDealerAndPlayer_DealerScoreWin() { + assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, LOW_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); + } +} From 024182727047b16416eef85cf2fab53a158d6075 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Thu, 2 Jul 2020 18:53:03 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20ReadyState=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20State=20=EC=83=81=EC=86=8D=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * NotFinishedState와 FinishedState로 구분 * 기존에 있던 Factory 클래스는 ReadyState로 대체 --- .../java/domain/participant/Participant.java | 20 +++++------ src/main/java/domain/participant/Player.java | 4 +++ .../participant/hand/BlackjackState.java | 5 +++ .../domain/participant/hand/BustedState.java | 5 +++ .../participant/hand/FinishedState.java | 12 ++++--- .../java/domain/participant/hand/Hand.java | 12 +++++-- .../domain/participant/hand/HandState.java | 20 +++++------ .../participant/hand/HandStateFactory.java | 14 -------- .../domain/participant/hand/HitState.java | 12 +------ .../participant/hand/NotFinishedState.java | 34 +++++++++++++++++++ .../domain/participant/hand/ReadyState.java | 21 ++++++++++++ .../domain/participant/hand/StartedState.java | 5 +++ .../domain/participant/hand/StayState.java | 10 ++++++ src/test/java/domain/Fixture.java | 12 ++++--- .../hand/HandStateFactoryTest.java | 29 ---------------- .../domain/participant/hand/HandTest.java | 2 +- .../participant/hand/ReadyStateTest.java | 34 +++++++++++++++++++ 17 files changed, 163 insertions(+), 88 deletions(-) delete mode 100644 src/main/java/domain/participant/hand/HandStateFactory.java create mode 100644 src/main/java/domain/participant/hand/NotFinishedState.java create mode 100644 src/main/java/domain/participant/hand/ReadyState.java delete mode 100644 src/test/java/domain/participant/hand/HandStateFactoryTest.java create mode 100644 src/test/java/domain/participant/hand/ReadyStateTest.java diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java index ec193af3911..091f5d04f15 100644 --- a/src/main/java/domain/participant/Participant.java +++ b/src/main/java/domain/participant/Participant.java @@ -4,13 +4,19 @@ import domain.Name; import domain.card.Card; -import domain.participant.hand.FinishedState; import domain.participant.hand.HandState; +import domain.participant.hand.ReadyState; public abstract class Participant { + public static final int INITIAL_DRAW_CARDS = 2; + private final Name name; HandState hand; + public Participant(final Name name) { + this(name, new ReadyState()); + } + public Participant(final Name name, final HandState hand) { this.name = Objects.requireNonNull(name, "name이 null입니다."); this.hand = Objects.requireNonNull(hand, "handState가 null입니다."); @@ -29,19 +35,11 @@ public boolean isFinished() { } public boolean isBusted() { - if (!isFinished()) { - return false; - } - FinishedState state = (FinishedState)hand; - return state.isBusted(); + return hand.isBusted(); } public boolean isBlackjack() { - if (!isFinished()) { - return false; - } - FinishedState state = (FinishedState)hand; - return state.isBlackjack(); + return hand.isBlackjack(); } public int compareScore(final Participant that) { diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java index e62ac82f56c..85255b2540e 100644 --- a/src/main/java/domain/participant/Player.java +++ b/src/main/java/domain/participant/Player.java @@ -18,4 +18,8 @@ public Player(final Name name, final HandState handState, final BettingMoney bet public BigDecimal calculateProfit() { return hand.calculateProfit(bettingMoney.getAmount()); } + + public BigDecimal getBettingMoney() { + return bettingMoney.getAmount(); + } } diff --git a/src/main/java/domain/participant/hand/BlackjackState.java b/src/main/java/domain/participant/hand/BlackjackState.java index e18ab882968..b9c07b66b93 100644 --- a/src/main/java/domain/participant/hand/BlackjackState.java +++ b/src/main/java/domain/participant/hand/BlackjackState.java @@ -14,6 +14,11 @@ public boolean isBlackjack() { return true; } + @Override + public boolean isBusted() { + return false; + } + @Override protected BigDecimal getProfitRate() { return PROFIT_RATE; diff --git a/src/main/java/domain/participant/hand/BustedState.java b/src/main/java/domain/participant/hand/BustedState.java index c541420aed1..e18e535ab11 100644 --- a/src/main/java/domain/participant/hand/BustedState.java +++ b/src/main/java/domain/participant/hand/BustedState.java @@ -9,6 +9,11 @@ public BustedState(final Hand hand) { super(hand); } + @Override + public boolean isBlackjack() { + return false; + } + @Override public boolean isBusted() { return true; diff --git a/src/main/java/domain/participant/hand/FinishedState.java b/src/main/java/domain/participant/hand/FinishedState.java index 98b6f613c93..49ec92c3ba8 100644 --- a/src/main/java/domain/participant/hand/FinishedState.java +++ b/src/main/java/domain/participant/hand/FinishedState.java @@ -3,6 +3,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import domain.card.Card; + public abstract class FinishedState extends StartedState { public FinishedState(final Hand hand) { super(hand); @@ -19,12 +21,14 @@ public boolean isFinished() { return true; } - public boolean isBlackjack() { - return false; + @Override + public HandState draw(final Card card) { + throw new UnsupportedOperationException("hit을 할 수 없는 상태입니다."); } - public boolean isBusted() { - return false; + @Override + public HandState stay() { + throw new UnsupportedOperationException("stay를 할 수 없는 상태입니다."); } protected abstract BigDecimal getProfitRate(); diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java index 8557ac89d5c..6760ea64d41 100644 --- a/src/main/java/domain/participant/hand/Hand.java +++ b/src/main/java/domain/participant/hand/Hand.java @@ -6,7 +6,7 @@ import domain.card.Card; public class Hand { - private static final int BLACKJACK_CARD_SIZE = 2; + private static final int INITIAL_CARD_SIZE = 2; private static final int BLACKJACK_SCORE = 21; private static final int ACE_UPGRADABLE_SCORE_UPPER_BOUND = 11; private static final int ACE_UPGRADE_SCORE = 10; @@ -22,7 +22,7 @@ public void draw(final Card card) { } public boolean isBlackjack() { - return cards.size() == BLACKJACK_CARD_SIZE && calculateScore() == BLACKJACK_SCORE; + return cards.size() == INITIAL_CARD_SIZE && calculateScore() == BLACKJACK_SCORE; } public boolean isBust() { @@ -34,6 +34,10 @@ public int calculateScore() { return upgradeIfHasAce(score); } + public int size() { + return cards.size(); + } + private int calculateMaxScore() { return cards.stream() .mapToInt(card -> card.getFace().getScore()) @@ -59,4 +63,8 @@ private boolean isAceUpgradableScore(final int score) { public List getCards() { return cards; } + + public boolean isInitialDraw() { + return cards.size() == INITIAL_CARD_SIZE; + } } diff --git a/src/main/java/domain/participant/hand/HandState.java b/src/main/java/domain/participant/hand/HandState.java index 16e785a14f8..6de492d25cc 100644 --- a/src/main/java/domain/participant/hand/HandState.java +++ b/src/main/java/domain/participant/hand/HandState.java @@ -5,25 +5,21 @@ import domain.card.Card; public interface HandState { - default HandState draw(final Card card) { - throw new UnsupportedOperationException("hit을 할 수 없는 상태입니다."); - } + HandState draw(final Card card); - default HandState stay() { - throw new UnsupportedOperationException("stay를 할 수 없는 상태입니다."); - } + HandState stay(); boolean isFinished(); - default boolean isOver(final int score) { - return calculateScore() > score; - } + boolean isBlackjack(); + + boolean isBusted(); + + boolean isOver(final int score); int calculateScore(); - default BigDecimal calculateProfit(final BigDecimal money) { - throw new UnsupportedOperationException("수익을 계산할 수 없는 상태입니다."); - } + BigDecimal calculateProfit(final BigDecimal money); Hand getHand(); } diff --git a/src/main/java/domain/participant/hand/HandStateFactory.java b/src/main/java/domain/participant/hand/HandStateFactory.java deleted file mode 100644 index 2a4e96fbd07..00000000000 --- a/src/main/java/domain/participant/hand/HandStateFactory.java +++ /dev/null @@ -1,14 +0,0 @@ -package domain.participant.hand; - -public class HandStateFactory { - private HandStateFactory() { - throw new AssertionError(); - } - - public static HandState createFromInitialHand(final Hand hand) { - if (hand.isBlackjack()) { - return new BlackjackState(hand); - } - return new HitState(hand); - } -} diff --git a/src/main/java/domain/participant/hand/HitState.java b/src/main/java/domain/participant/hand/HitState.java index fe3182f1b0c..592a4b9281b 100644 --- a/src/main/java/domain/participant/hand/HitState.java +++ b/src/main/java/domain/participant/hand/HitState.java @@ -2,7 +2,7 @@ import domain.card.Card; -public class HitState extends StartedState { +public class HitState extends NotFinishedState { public HitState(final Hand hand) { super(hand); } @@ -15,14 +15,4 @@ public HandState draw(final Card card) { } return new HitState(hand); } - - @Override - public HandState stay() { - return new StayState(hand); - } - - @Override - public boolean isFinished() { - return false; - } } diff --git a/src/main/java/domain/participant/hand/NotFinishedState.java b/src/main/java/domain/participant/hand/NotFinishedState.java new file mode 100644 index 00000000000..c42b11c87f0 --- /dev/null +++ b/src/main/java/domain/participant/hand/NotFinishedState.java @@ -0,0 +1,34 @@ +package domain.participant.hand; + +import java.math.BigDecimal; + +public abstract class NotFinishedState extends StartedState { + public NotFinishedState(final Hand hand) { + super(hand); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBusted() { + return false; + } + + @Override + public HandState stay() { + return new StayState(hand); + } + + @Override + public BigDecimal calculateProfit(final BigDecimal money) { + throw new UnsupportedOperationException("수익을 계산할 수 없는 상태입니다."); + } +} diff --git a/src/main/java/domain/participant/hand/ReadyState.java b/src/main/java/domain/participant/hand/ReadyState.java new file mode 100644 index 00000000000..3094fd5db53 --- /dev/null +++ b/src/main/java/domain/participant/hand/ReadyState.java @@ -0,0 +1,21 @@ +package domain.participant.hand; + +import domain.card.Card; + +public class ReadyState extends NotFinishedState { + public ReadyState() { + super(new Hand()); + } + + @Override + public HandState draw(final Card card) { + hand.draw(card); + if (hand.isBlackjack()) { + return new BlackjackState(hand); + } + if (hand.isInitialDraw()) { + return new HitState(hand); + } + return this; + } +} diff --git a/src/main/java/domain/participant/hand/StartedState.java b/src/main/java/domain/participant/hand/StartedState.java index fd2586d100b..b97676ef5f6 100644 --- a/src/main/java/domain/participant/hand/StartedState.java +++ b/src/main/java/domain/participant/hand/StartedState.java @@ -7,6 +7,11 @@ public StartedState(final Hand hand) { this.hand = hand; } + @Override + public boolean isOver(final int score) { + return calculateScore() > score; + } + @Override public int calculateScore() { return hand.calculateScore(); diff --git a/src/main/java/domain/participant/hand/StayState.java b/src/main/java/domain/participant/hand/StayState.java index 32fb4566ec5..f25896c7154 100644 --- a/src/main/java/domain/participant/hand/StayState.java +++ b/src/main/java/domain/participant/hand/StayState.java @@ -13,4 +13,14 @@ public StayState(final Hand hand) { protected BigDecimal getProfitRate() { return PROFIT_RATE; } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public boolean isBusted() { + return false; + } } diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index f77dd6eeca4..5cce82f008d 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -17,7 +17,7 @@ import domain.card.Card; import domain.participant.hand.Hand; import domain.participant.hand.HandState; -import domain.participant.hand.HandStateFactory; +import domain.participant.hand.ReadyState; public class Fixture { public static final List CARDS = Card.values(); @@ -54,8 +54,8 @@ public class Fixture { public static final Hand BLACKJACK_HAND = createHand(BLACKJACK_CARDS); public static final Hand BUSTED_HAND = createHand(BUSTED_CARDS); - public static final HandState HITTABLE_HAND_STATE = HandStateFactory.createFromInitialHand(HITTABLE_HIGH_HAND); - public static final HandState BLACKJACK_HAND_STATE = HandStateFactory.createFromInitialHand(BLACKJACK_HAND); + public static final HandState HITTABLE_HAND_STATE = createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS); + public static final HandState BLACKJACK_HAND_STATE = createHandState(BLACKJACK_CARDS); public static Hand createHand(List cards) { Hand hand = new Hand(); @@ -66,6 +66,10 @@ public static Hand createHand(List cards) { } public static HandState createHandState(List cards) { - return HandStateFactory.createFromInitialHand(createHand(cards)); + HandState handState = new ReadyState(); + for (final Card card : cards) { + handState = handState.draw(card); + } + return handState; } } diff --git a/src/test/java/domain/participant/hand/HandStateFactoryTest.java b/src/test/java/domain/participant/hand/HandStateFactoryTest.java deleted file mode 100644 index e828525692d..00000000000 --- a/src/test/java/domain/participant/hand/HandStateFactoryTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package domain.participant.hand; - -import static domain.Fixture.BLACKJACK_CARDS; -import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; -import static domain.Fixture.createHand; -import static org.assertj.core.api.Assertions.assertThat; - -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 HandStateFactoryTest { - private static Stream createCardsAndExpectResult() { - return Stream.of( - Arguments.of(createHand(BLACKJACK_CARDS), BlackjackState.class), - Arguments.of(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS), HitState.class) - ); - } - - @DisplayName("createFromInitialHand: 카드를 입력받아 초기 상태 생성") - @MethodSource("createCardsAndExpectResult") - @ParameterizedTest - void createFromInitialHand(final Hand hand, final Class expect) { - assertThat(HandStateFactory.createFromInitialHand(hand)).isInstanceOf(expect); - } -} \ No newline at end of file diff --git a/src/test/java/domain/participant/hand/HandTest.java b/src/test/java/domain/participant/hand/HandTest.java index 2bfb331e2d6..4a87d522018 100644 --- a/src/test/java/domain/participant/hand/HandTest.java +++ b/src/test/java/domain/participant/hand/HandTest.java @@ -64,7 +64,7 @@ void draw() { Card card = Card.fromFaceAndSuit(ACE, SPADE); hand.draw(card); - assertThat(hand.getCards()).hasSize(1); + assertThat(hand.size()).isEqualTo(1); } @DisplayName("isBlackjack: 카드가 블랙잭인지 여부를 판단") diff --git a/src/test/java/domain/participant/hand/ReadyStateTest.java b/src/test/java/domain/participant/hand/ReadyStateTest.java new file mode 100644 index 00000000000..41c311327b2 --- /dev/null +++ b/src/test/java/domain/participant/hand/ReadyStateTest.java @@ -0,0 +1,34 @@ +package domain.participant.hand; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ReadyStateTest { + private ReadyState readyState; + + @BeforeEach + void setUp() { + readyState = new ReadyState(); + } + + @DisplayName("Ready 상태 생성") + @Test + void constructor() { + assertThat(new ReadyState()).isInstanceOf(ReadyState.class); + } + + @DisplayName("블랙잭이 아님을 확인") + @Test + void isBlackjack() { + assertThat(readyState.isBlackjack()).isFalse(); + } + + @DisplayName("버스트가 아님을 확인") + @Test + void isBusted() { + assertThat(readyState.isBusted()).isFalse(); + } +} From 8479b88aa009dbad31fc72bc95e4e9f30080669e Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Thu, 2 Jul 2020 19:00:24 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=9D=B5=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/domain/participant/hand/Hand.java | 8 ++-- src/main/java/domain/result/GameResult.java | 17 +++++-- .../java/domain/result/GameResultTest.java | 44 +++++++++++++++---- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java index 6760ea64d41..11d6c7d8c80 100644 --- a/src/main/java/domain/participant/hand/Hand.java +++ b/src/main/java/domain/participant/hand/Hand.java @@ -60,11 +60,11 @@ private boolean isAceUpgradableScore(final int score) { return score <= ACE_UPGRADABLE_SCORE_UPPER_BOUND; } - public List getCards() { - return cards; - } - public boolean isInitialDraw() { return cards.size() == INITIAL_CARD_SIZE; } + + public List getCards() { + return cards; + } } diff --git a/src/main/java/domain/result/GameResult.java b/src/main/java/domain/result/GameResult.java index d7e8b1f7eee..c61c09fbf4c 100644 --- a/src/main/java/domain/result/GameResult.java +++ b/src/main/java/domain/result/GameResult.java @@ -1,19 +1,24 @@ package domain.result; +import java.math.BigDecimal; +import java.util.function.Function; import java.util.stream.Stream; import domain.participant.Dealer; import domain.participant.Player; public enum GameResult { - PLAYER_WIN(new PlayerWinStrategy()), - DRAW(new DrawStrategy()), - PLAYER_LOSE(new PlayerLoseStrategy()); + PLAYER_WIN(new PlayerWinStrategy(), Player::calculateProfit), + DRAW(new DrawStrategy(), player -> BigDecimal.ZERO), + PLAYER_LOSE(new PlayerLoseStrategy(), player -> player.getBettingMoney().multiply(BigDecimal.valueOf(-1))); private final ResultDecideStrategy resultDecideStrategy; + private final Function profitCalculateFunction; - GameResult(final ResultDecideStrategy resultDecideStrategy) { + GameResult(final ResultDecideStrategy resultDecideStrategy, + final Function profitCalculateFunction) { this.resultDecideStrategy = resultDecideStrategy; + this.profitCalculateFunction = profitCalculateFunction; } public static GameResult fromDealerAndPlayer(final Dealer dealer, final Player player) { @@ -23,6 +28,10 @@ public static GameResult fromDealerAndPlayer(final Dealer dealer, final Player p .orElseThrow(() -> new IllegalArgumentException("결과가 존재하지 않습니다.")); } + public BigDecimal calculateProfit(final Player player) { + return profitCalculateFunction.apply(player); + } + private boolean matches(final Dealer dealer, final Player player) { return resultDecideStrategy.matches(dealer, player); } diff --git a/src/test/java/domain/result/GameResultTest.java b/src/test/java/domain/result/GameResultTest.java index aa0a0788faa..b631a175b3a 100644 --- a/src/test/java/domain/result/GameResultTest.java +++ b/src/test/java/domain/result/GameResultTest.java @@ -15,6 +15,8 @@ import static domain.result.GameResult.fromDealerAndPlayer; import static org.assertj.core.api.Assertions.assertThat; +import java.math.BigDecimal; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -39,51 +41,77 @@ public class GameResultTest { private static final Player BUSTED_PLAYER = new Player(NORTHJUN, new BustedState(BUSTED_HAND), HUNDRED_BETTING_MONEY); - @DisplayName("플레이어가 블랙잭 승리") + @DisplayName("fromDealerAndPlayer: 플레이어가 블랙잭 승리") @Test void fromDealerAndPlayer_PlayerBlackjackWin() { assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, BLACKJACK_PLAYER)).isEqualTo(PLAYER_WIN); } - @DisplayName("블랙잭 무승부") + @DisplayName("fromDealerAndPlayer: 블랙잭 무승부") @Test void fromDealerAndPlayer_BlackjackDraw() { assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, BLACKJACK_PLAYER)).isEqualTo(DRAW); } - @DisplayName("딜러가 블랙잭 승리") + @DisplayName("fromDealerAndPlayer: 딜러가 블랙잭 승리") @Test void fromDealerAndPlayer_DealerBlackjackWin() { assertThat(fromDealerAndPlayer(BLACKJACK_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); } - @DisplayName("플레이어가 버스트 패배") + @DisplayName("fromDealerAndPlayer: 플레이어가 버스트 패배") @Test void fromDealerAndPlayer_PlayerBustLose() { assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, BUSTED_PLAYER)).isEqualTo(PLAYER_LOSE); } - @DisplayName("딜러가 버스트 패배") + @DisplayName("fromDealerAndPlayer: 딜러가 버스트 패배") @Test void fromDealerAndPlayer_DealerBustLose() { assertThat(fromDealerAndPlayer(BUSTED_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); } - @DisplayName("플레이어가 점수를 비교하여 승리") + @DisplayName("fromDealerAndPlayer: 플레이어가 점수를 비교하여 승리") @Test void fromDealerAndPlayer_PlayerScoreWin() { assertThat(fromDealerAndPlayer(LOW_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(PLAYER_WIN); } - @DisplayName("점수를 비교하여 무승부") + @DisplayName("fromDealerAndPlayer: 점수를 비교하여 무승부") @Test void fromDealerAndPlayer_ScoreDraw() { assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, HIGH_SCORE_PLAYER)).isEqualTo(DRAW); } - @DisplayName("딜러가 점수를 비교하여 승리") + @DisplayName("fromDealerAndPlayer: 딜러가 점수를 비교하여 승리") @Test void fromDealerAndPlayer_DealerScoreWin() { assertThat(fromDealerAndPlayer(HIGH_SCORE_DEALER, LOW_SCORE_PLAYER)).isEqualTo(PLAYER_LOSE); } + + @DisplayName("calculateProfit: 플레이어가 블랙잭으로 승리했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsBlackjackWin() { + assertThat(PLAYER_WIN.calculateProfit(BLACKJACK_PLAYER)).isEqualTo(BigDecimal.valueOf(1_500_000)); + } + + @DisplayName("calculateProfit: 플레이어가 승리했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsWin() { + assertThat(PLAYER_WIN.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(1_000_000)); + + } + + @DisplayName("calculateProfit: 무승부일 때 수익 계산") + @Test + void calculateProfit_Draw() { + assertThat(DRAW.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(0)); + + } + + @DisplayName("calculateProfit: 플레이어가 패배했을 때 수익 계산") + @Test + void calculateProfit_PlayerIsLose() { + assertThat(PLAYER_LOSE.calculateProfit(HIGH_SCORE_PLAYER)).isEqualTo(BigDecimal.valueOf(-1 * 1_000_000)); + } } From 83f58477d9248115675ea7be8938a9119827a95f Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Thu, 2 Jul 2020 22:38:27 +0900 Subject: [PATCH 15/17] =?UTF-8?q?feat:=20Profit=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 +++++----- src/main/java/domain/card/Card.java | 4 ++ src/main/java/domain/card/CardDeck.java | 4 -- .../java/domain/card/CardDeckFactory.java | 11 ++++ .../domain/card/EmptyCardDeckException.java | 7 +++ src/main/java/domain/card/Face.java | 19 +++++-- src/main/java/domain/card/RandomCardDeck.java | 13 +---- src/main/java/domain/card/Suit.java | 18 ++++-- src/main/java/domain/participant/Dealer.java | 12 ++-- .../java/domain/participant/Participant.java | 21 ++++--- src/main/java/domain/participant/Player.java | 5 ++ .../domain/participant/PlayerDecision.java | 34 +++++++++++ .../java/domain/participant/hand/Hand.java | 10 +++- .../domain/participant/hand/HitState.java | 3 + .../java/domain/result/ParticipantProfit.java | 23 ++++++++ src/main/java/service/DrawService.java | 26 +++++++++ src/main/java/service/PlayerService.java | 39 +++++++++++++ src/main/java/service/ProfitService.java | 30 ++++++++++ src/test/java/domain/Fixture.java | 4 -- .../java/domain/card/CardDeckFactoryTest.java | 13 +++++ src/test/java/domain/card/CardTest.java | 9 +++ src/test/java/domain/card/FakeCardDeck.java | 22 ++++++++ .../java/domain/card/RandomCardDeckTest.java | 14 +++-- .../java/domain/participant/DealerTest.java | 56 ++++++++++++++++--- .../participant/PlayerDecisionTest.java | 41 ++++++++++++++ .../java/domain/participant/PlayerTest.java | 41 +++++++++----- .../participant/hand/BlackjackStateTest.java | 6 ++ .../domain/participant/hand/HandTest.java | 34 +++++++++++ .../domain/participant/hand/HitStateTest.java | 25 ++++++--- .../participant/hand/ReadyStateTest.java | 18 +++++- .../domain/result/ParticipantProfitTest.java | 43 ++++++++++++++ 31 files changed, 542 insertions(+), 91 deletions(-) create mode 100644 src/main/java/domain/card/CardDeckFactory.java create mode 100644 src/main/java/domain/card/EmptyCardDeckException.java create mode 100644 src/main/java/domain/participant/PlayerDecision.java create mode 100644 src/main/java/domain/result/ParticipantProfit.java create mode 100644 src/main/java/service/DrawService.java create mode 100644 src/main/java/service/PlayerService.java create mode 100644 src/main/java/service/ProfitService.java create mode 100644 src/test/java/domain/card/CardDeckFactoryTest.java create mode 100644 src/test/java/domain/card/FakeCardDeck.java create mode 100644 src/test/java/domain/participant/PlayerDecisionTest.java create mode 100644 src/test/java/domain/result/ParticipantProfitTest.java diff --git a/README.md b/README.md index db306dca80e..9f8ffb20dd2 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,24 @@ * [X] 참여자의 이름은 1 ~ 10자로 구성된다. * [X] 참여자의 이름 앞, 뒤에 오는 공백은 무시한다. * [X] 참여자의 이름은 comma(,) 단위로 구분한다. - * [ ] 참여자의 수는 2 ~ 8명이다. -* [ ] 각 참여자의 베팅 금액을 입력받는다. + * [X] 참여자의 수는 2 ~ 8명이다. +* [X] 각 참여자의 베팅 금액을 입력받는다. * [X] 베팅 금액의 최소 단위는 100으로 제한한다. -* [ ] 딜러와 각 참여자에게 카드를 두 장씩 분배한다. -* [ ] 각 참여자에게 나누어 준 카드를 출력한다. - * [ ] 딜러는 첫 번째 카드를 제외하고 한 장의 카드만 공개한다. - * [ ] 각 참여자는 두 장의 카드를 공개한다. +* [X] 딜러와 각 참여자에게 카드를 두 장씩 분배한다. +* [X] 각 참여자에게 나누어 준 카드를 출력한다. + * [X] 딜러는 첫 번째 카드를 제외하고 한 장의 카드만 공개한다. + * [X] 각 참여자는 두 장의 카드를 공개한다. * 딜러가 블랙잭인지 여부를 판별하고, 블랙잭이면 결과를 출력한다. - * [ ] 참여자가 블랙잭이라면 무승부이다. - * [ ] 참여자가 블랙잭이 아니라면 딜러의 승리이다. + * [X] 참여자가 블랙잭이라면 무승부이다. + * [X] 참여자가 블랙잭이 아니라면 딜러의 승리이다. * 참여자 히트 - * [ ] 참여자가 블랙잭, 버스트가 아니라면 추가적으로 카드를 히트할지 여부를 입력받는다. - * [ ] 참여자가 스테이하면 다음 사람 턴으로 넘어간다. - * [ ] 참여자가 히트하면 카드 발급 후 카드를 출력한다. + * [X] 참여자가 블랙잭, 버스트가 아니라면 추가적으로 카드를 히트할지 여부를 입력받는다. + * [X] 참여자가 스테이하면 다음 사람 턴으로 넘어간다. + * [X] 참여자가 히트하면 카드 발급 후 카드를 출력한다. * 딜러 히트 - * [ ] 딜러는 가진 패의 합이 16 이하라면 반드시 히트해야 한다. -* [ ] 딜러와 참여자가 받은 모든 패를 공개하고, 결과를 출력한다. -* [ ] 각 참여자의 최종 수익을 출력한다. + * [X] 딜러는 가진 패의 합이 16 이하라면 반드시 히트해야 한다. +* [X] 딜러와 참여자가 받은 모든 패를 공개하고, 결과를 출력한다. +* [X] 각 참여자의 최종 수익을 출력한다. ## 게임 조건 * 두 장의 패를 뽑아 합이 21인 경우 블랙잭이다. diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java index c972f79eace..28b907cebee 100644 --- a/src/main/java/domain/card/Card.java +++ b/src/main/java/domain/card/Card.java @@ -45,6 +45,10 @@ public Suit getSuit() { return suit; } + public String alias() { + return suit.alias() + face.alias(); + } + private static class CardCache { public static final List cache; diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java index 2d0bc379ea0..b67198cfde1 100644 --- a/src/main/java/domain/card/CardDeck.java +++ b/src/main/java/domain/card/CardDeck.java @@ -1,9 +1,5 @@ package domain.card; -import java.util.List; - public interface CardDeck { Card pick(); - - List pick(final int count); } diff --git a/src/main/java/domain/card/CardDeckFactory.java b/src/main/java/domain/card/CardDeckFactory.java new file mode 100644 index 00000000000..7d96f10703b --- /dev/null +++ b/src/main/java/domain/card/CardDeckFactory.java @@ -0,0 +1,11 @@ +package domain.card; + +public class CardDeckFactory { + private CardDeckFactory() { + throw new AssertionError(); + } + + public static CardDeck createRandomCardDeck() { + return new RandomCardDeck(Card.values()); + } +} diff --git a/src/main/java/domain/card/EmptyCardDeckException.java b/src/main/java/domain/card/EmptyCardDeckException.java new file mode 100644 index 00000000000..370477b8100 --- /dev/null +++ b/src/main/java/domain/card/EmptyCardDeckException.java @@ -0,0 +1,7 @@ +package domain.card; + +public class EmptyCardDeckException extends RuntimeException { + public EmptyCardDeckException(final String message) { + super(message); + } +} diff --git a/src/main/java/domain/card/Face.java b/src/main/java/domain/card/Face.java index b95555f2655..cf6ccff8fbd 100644 --- a/src/main/java/domain/card/Face.java +++ b/src/main/java/domain/card/Face.java @@ -1,7 +1,7 @@ package domain.card; public enum Face { - ACE(1), + ACE(1, "A"), TWO(2), THREE(3), FOUR(4), @@ -11,14 +11,21 @@ public enum Face { EIGHT(8), NINE(9), TEN(10), - JACK(10), - QUEEN(10), - KING(10); + JACK(10, "J"), + QUEEN(10, "Q"), + KING(10, "K"); private final int score; + private final String alias; Face(final int score) { this.score = score; + this.alias = String.valueOf(score); + } + + Face(final int score, final String alias) { + this.score = score; + this.alias = alias; } public boolean isAce() { @@ -28,4 +35,8 @@ public boolean isAce() { public int getScore() { return score; } + + public String alias() { + return alias; + } } diff --git a/src/main/java/domain/card/RandomCardDeck.java b/src/main/java/domain/card/RandomCardDeck.java index 8c38cbb01a7..32dbf6e2750 100644 --- a/src/main/java/domain/card/RandomCardDeck.java +++ b/src/main/java/domain/card/RandomCardDeck.java @@ -1,6 +1,5 @@ package domain.card; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -19,16 +18,10 @@ public RandomCardDeck(final List cards) { @Override public Card pick() { - return cards.pop(); - } - - @Override - public List pick(final int amount) { - List pickedCards = new ArrayList<>(); - for (int count = 0; count < amount; ++count) { - pickedCards.add(pick()); + if (cards.isEmpty()) { + throw new EmptyCardDeckException("카드를 모두 소모하여 더 뽑을 수 없습니다."); } - return pickedCards; + return cards.pop(); } private void validate(final List cards) { diff --git a/src/main/java/domain/card/Suit.java b/src/main/java/domain/card/Suit.java index 9015751f019..83cc505e3db 100644 --- a/src/main/java/domain/card/Suit.java +++ b/src/main/java/domain/card/Suit.java @@ -1,8 +1,18 @@ package domain.card; public enum Suit { - SPADE, - HEART, - DIAMOND, - CLUB + SPADE("♠️"), + HEART("❤️"), + DIAMOND("♦️"), + CLUB("♣️"); + + private final String alias; + + Suit(final String alias) { + this.alias = alias; + } + + public String alias() { + return alias; + } } diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java index 11dafff7a11..0a3dd3f19db 100644 --- a/src/main/java/domain/participant/Dealer.java +++ b/src/main/java/domain/participant/Dealer.java @@ -1,21 +1,25 @@ package domain.participant; import domain.Name; -import domain.card.Card; +import domain.card.CardDeck; import domain.participant.hand.HandState; public class Dealer extends Participant { private static final Name NAME = new Name("딜러"); private static final int DEALER_HITTABLE_UPPER_BOUND = 16; + public Dealer() { + super(NAME); + } + public Dealer(final HandState hand) { super(NAME, hand); } @Override - public void hit(final Card card) { - super.hit(card); - if (hand.isOver(DEALER_HITTABLE_UPPER_BOUND)) { + public void hit(final CardDeck cardDeck) { + super.hit(cardDeck); + if (!hand.isFinished() && hand.isOver(DEALER_HITTABLE_UPPER_BOUND)) { stay(); } } diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java index 091f5d04f15..23543c1b7c2 100644 --- a/src/main/java/domain/participant/Participant.java +++ b/src/main/java/domain/participant/Participant.java @@ -3,7 +3,8 @@ import java.util.Objects; import domain.Name; -import domain.card.Card; +import domain.card.CardDeck; +import domain.participant.hand.Hand; import domain.participant.hand.HandState; import domain.participant.hand.ReadyState; @@ -22,8 +23,14 @@ public Participant(final Name name, final HandState hand) { this.hand = Objects.requireNonNull(hand, "handState가 null입니다."); } - public void hit(final Card card) { - hand = hand.draw(card); + public void hitAtFirst(final CardDeck cardDeck) { + for (int count = 0; count < INITIAL_DRAW_CARDS; count++) { + hit(cardDeck); + } + } + + public void hit(final CardDeck cardDeck) { + hand = hand.draw(cardDeck.pick()); } public void stay() { @@ -50,11 +57,11 @@ private int calculateScore() { return hand.calculateScore(); } - public Name getName() { - return name; + public String getName() { + return name.getName(); } - public HandState getHandState() { - return hand; + public Hand getHand() { + return hand.getHand(); } } diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java index 85255b2540e..4df8c3f6df7 100644 --- a/src/main/java/domain/participant/Player.java +++ b/src/main/java/domain/participant/Player.java @@ -10,6 +10,11 @@ public class Player extends Participant { private final BettingMoney bettingMoney; + public Player(final Name name, final BettingMoney bettingMoney) { + super(name); + this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); + } + public Player(final Name name, final HandState handState, final BettingMoney bettingMoney) { super(name, handState); this.bettingMoney = Objects.requireNonNull(bettingMoney, "bettingMoney가 null입니다."); diff --git a/src/main/java/domain/participant/PlayerDecision.java b/src/main/java/domain/participant/PlayerDecision.java new file mode 100644 index 00000000000..8df703f7110 --- /dev/null +++ b/src/main/java/domain/participant/PlayerDecision.java @@ -0,0 +1,34 @@ +package domain.participant; + +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +import domain.card.CardDeck; + +public enum PlayerDecision { + HIT("y", (cardDeck, player) -> player.hit(cardDeck)), + STAY("n", (cardDeck, player) -> player.stay()); + + private final String alias; + private final BiConsumer playerAction; + + PlayerDecision(final String alias, final BiConsumer playerAction) { + this.alias = alias; + this.playerAction = playerAction; + } + + public static PlayerDecision of(final String input) { + return Stream.of(values()) + .filter(alias -> alias.isAliasOf(input)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 입력입니다.")); + } + + public void act(final CardDeck cardDeck, final Player player) { + playerAction.accept(cardDeck, player); + } + + private boolean isAliasOf(final String input) { + return alias.equals(input); + } +} diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java index 11d6c7d8c80..568d50e02e2 100644 --- a/src/main/java/domain/participant/hand/Hand.java +++ b/src/main/java/domain/participant/hand/Hand.java @@ -7,7 +7,7 @@ public class Hand { private static final int INITIAL_CARD_SIZE = 2; - private static final int BLACKJACK_SCORE = 21; + private static final int MAX_SCORE = 21; private static final int ACE_UPGRADABLE_SCORE_UPPER_BOUND = 11; private static final int ACE_UPGRADE_SCORE = 10; @@ -22,11 +22,15 @@ public void draw(final Card card) { } public boolean isBlackjack() { - return cards.size() == INITIAL_CARD_SIZE && calculateScore() == BLACKJACK_SCORE; + return isInitialDraw() && isMaxScore(); + } + + public boolean isMaxScore() { + return calculateScore() == MAX_SCORE; } public boolean isBust() { - return calculateScore() > BLACKJACK_SCORE; + return calculateScore() > MAX_SCORE; } public int calculateScore() { diff --git a/src/main/java/domain/participant/hand/HitState.java b/src/main/java/domain/participant/hand/HitState.java index 592a4b9281b..80653bdf2db 100644 --- a/src/main/java/domain/participant/hand/HitState.java +++ b/src/main/java/domain/participant/hand/HitState.java @@ -10,6 +10,9 @@ public HitState(final Hand hand) { @Override public HandState draw(final Card card) { hand.draw(card); + if (hand.isMaxScore()) { + return new StayState(hand); + } if (hand.isBust()) { return new BustedState(hand); } diff --git a/src/main/java/domain/result/ParticipantProfit.java b/src/main/java/domain/result/ParticipantProfit.java new file mode 100644 index 00000000000..c3dd3c72448 --- /dev/null +++ b/src/main/java/domain/result/ParticipantProfit.java @@ -0,0 +1,23 @@ +package domain.result; + +import java.math.BigDecimal; + +import domain.participant.Participant; + +public class ParticipantProfit { + private final String name; + private final BigDecimal profit; + + public ParticipantProfit(final Participant participant, final BigDecimal profit) { + this.name = participant.getName(); + this.profit = profit; + } + + public String getName() { + return name; + } + + public BigDecimal getProfit() { + return profit; + } +} diff --git a/src/main/java/service/DrawService.java b/src/main/java/service/DrawService.java new file mode 100644 index 00000000000..71d001fe570 --- /dev/null +++ b/src/main/java/service/DrawService.java @@ -0,0 +1,26 @@ +package service; + +import java.util.List; + +import domain.card.CardDeck; +import domain.participant.Dealer; +import domain.participant.Player; +import domain.participant.PlayerDecision; + +public class DrawService { + public void drawInitialCards(final CardDeck cardDeck, final Dealer dealer, final List players) { + dealer.hitAtFirst(cardDeck); + for (final Player player : players) { + player.hitAtFirst(cardDeck); + } + } + + public void drawDealer(final CardDeck cardDeck, final Dealer dealer) { + dealer.hit(cardDeck); + } + + public void drawPlayer(final CardDeck cardDeck, final Player player, final String decision) { + PlayerDecision playerDecision = PlayerDecision.of(decision); + playerDecision.act(cardDeck, player); + } +} diff --git a/src/main/java/service/PlayerService.java b/src/main/java/service/PlayerService.java new file mode 100644 index 00000000000..d3d5246a138 --- /dev/null +++ b/src/main/java/service/PlayerService.java @@ -0,0 +1,39 @@ +package service; + +import java.util.ArrayList; +import java.util.List; + +import domain.BettingMoney; +import domain.Name; +import domain.participant.Player; + +public class PlayerService { + private static final int MIN_PLAYERS = 2; + private static final int MAX_PLAYERS = 8; + + public List createPlayers(List names, List bettingMonies) { + validateSize(names, bettingMonies); + validateLength(names); + List players = new ArrayList<>(); + for (int count = 0; count < names.size(); count++) { + players.add(new Player(names.get(count), bettingMonies.get(count))); + } + return players; + } + + private void validateSize(final List names, final List bettingMonies) { + if (names.size() != bettingMonies.size()) { + throw new IllegalArgumentException("names와 bettingMonies의 크기가 일치하지 않습니다.\n" + + "names size: " + names.size() + "\n" + + "bettingMonies size: " + bettingMonies.size()); + } + } + + private void validateLength(final List names) { + int totalPlayer = names.size(); + if (totalPlayer < MIN_PLAYERS || totalPlayer > MAX_PLAYERS) { + throw new IllegalArgumentException("참여자의 수가 올바르지 않습니다.\n" + + "players size: " + names.size()); + } + } +} diff --git a/src/main/java/service/ProfitService.java b/src/main/java/service/ProfitService.java new file mode 100644 index 00000000000..3067f09c63f --- /dev/null +++ b/src/main/java/service/ProfitService.java @@ -0,0 +1,30 @@ +package service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import domain.participant.Dealer; +import domain.participant.Player; +import domain.result.GameResult; +import domain.result.ParticipantProfit; + +public class ProfitService { + public List calculateProfits(final Dealer dealer, final List players) { + List participantProfits = new ArrayList<>(); + for (final Player player : players) { + GameResult gameResult = GameResult.fromDealerAndPlayer(dealer, player); + BigDecimal profit = gameResult.calculateProfit(player); + participantProfits.add(new ParticipantProfit(player, profit)); + } + ParticipantProfit dealerProfit = new ParticipantProfit(dealer, calculateDealerProfit(participantProfits)); + participantProfits.add(0, dealerProfit); + return participantProfits; + } + + private BigDecimal calculateDealerProfit(final List profits) { + return profits.stream() + .map(ParticipantProfit::getProfit) + .reduce(BigDecimal.ZERO, BigDecimal::subtract); + } +} diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index 5cce82f008d..397751739ba 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -27,7 +27,6 @@ public class Fixture { public static final Name WESTJUN = new Name("westjun"); public static final Name EASTJUN = new Name("eastjun"); - public static final BettingMoney THOUSAND_BETTING_MONEY = new BettingMoney(1_000); public static final BettingMoney HUNDRED_BETTING_MONEY = new BettingMoney(1_000_000); public static final Card ACE_SCORE = Card.fromFaceAndSuit(ACE, CLUB); @@ -37,8 +36,6 @@ public class Fixture { public static final Card FIVE_SCORE = Card.fromFaceAndSuit(FIVE, CLUB); public static final Card SIX_SCORE = Card.fromFaceAndSuit(SIX, CLUB); public static final Card SEVEN_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); - public static final Card EIGHT_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); - public static final Card NINE_SCORE = Card.fromFaceAndSuit(SEVEN, CLUB); public static final Card TEN_SCORE = Card.fromFaceAndSuit(JACK, SPADE); public static final List BUSTED_CARDS = Arrays.asList(TEN_SCORE, TEN_SCORE, TWO_SCORE); @@ -55,7 +52,6 @@ public class Fixture { public static final Hand BUSTED_HAND = createHand(BUSTED_CARDS); public static final HandState HITTABLE_HAND_STATE = createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS); - public static final HandState BLACKJACK_HAND_STATE = createHandState(BLACKJACK_CARDS); public static Hand createHand(List cards) { Hand hand = new Hand(); diff --git a/src/test/java/domain/card/CardDeckFactoryTest.java b/src/test/java/domain/card/CardDeckFactoryTest.java new file mode 100644 index 00000000000..e1dbecdbfcb --- /dev/null +++ b/src/test/java/domain/card/CardDeckFactoryTest.java @@ -0,0 +1,13 @@ +package domain.card; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CardDeckFactoryTest { + @DisplayName("constructor: 랜덤 카드 덱 생성") + @Test + void createRandomCardDeck() { + Assertions.assertThat(CardDeckFactory.createRandomCardDeck()).isInstanceOf(RandomCardDeck.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java index edc295d840c..8503dc6a931 100644 --- a/src/test/java/domain/card/CardTest.java +++ b/src/test/java/domain/card/CardTest.java @@ -2,6 +2,7 @@ import static domain.card.Face.ACE; import static domain.card.Suit.CLUB; +import static domain.card.Suit.HEART; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -65,4 +66,12 @@ void getSuit() { assertThat(card.getSuit()).isEqualTo(CLUB); } + + @DisplayName("alias: 출력용 텍스트 반환") + @Test + void alias() { + Card card = Card.fromFaceAndSuit(ACE, HEART); + + assertThat(card.alias()).isEqualTo("❤️A"); + } } diff --git a/src/test/java/domain/card/FakeCardDeck.java b/src/test/java/domain/card/FakeCardDeck.java new file mode 100644 index 00000000000..0fb7b97b869 --- /dev/null +++ b/src/test/java/domain/card/FakeCardDeck.java @@ -0,0 +1,22 @@ +package domain.card; + +import java.util.List; +import java.util.Stack; + +public class FakeCardDeck implements CardDeck { + private final Stack cards; + + public FakeCardDeck(final List cards) { + Stack stackedCards = new Stack<>(); + stackedCards.addAll(cards); + this.cards = stackedCards; + } + + @Override + public Card pick() { + if (cards.isEmpty()) { + throw new EmptyCardDeckException("카드를 모두 소모하여 더 뽑을 수 없습니다."); + } + return cards.pop(); + } +} diff --git a/src/test/java/domain/card/RandomCardDeckTest.java b/src/test/java/domain/card/RandomCardDeckTest.java index 4c834fb050f..0770be39eca 100644 --- a/src/test/java/domain/card/RandomCardDeckTest.java +++ b/src/test/java/domain/card/RandomCardDeckTest.java @@ -1,10 +1,12 @@ package domain.card; +import static domain.Fixture.ACE_SCORE; import static domain.Fixture.CARDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; +import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -22,7 +24,6 @@ void setUp() { @Test void constructor() { assertThat(new RandomCardDeck(Card.values())).isInstanceOf(RandomCardDeck.class); - } @DisplayName("constructor: 카드 리스트가 null인 경우 예외 발생") @@ -47,9 +48,14 @@ void pick() { assertThat(cardDeck.pick()).isInstanceOf(Card.class); } - @DisplayName("pick: 카드를 입력받은 장수만큼 뽑음") + @DisplayName("pick: 카드 한 장을 뽑을 때 더 이상 카드가 없으면 예외 발생") @Test - void pick_MultipleCards() { - assertThat(cardDeck.pick(5)).hasSize(5); + void pick_DeckIsEmpty_ExceptionThrown() { + cardDeck = new RandomCardDeck(Collections.singletonList(ACE_SCORE)); + cardDeck.pick(); + + assertThatThrownBy(() -> cardDeck.pick()) + .isInstanceOf(EmptyCardDeckException.class) + .hasMessageContaining("카드를 모두 소모하여 더 뽑을 수 없습니다"); } } diff --git a/src/test/java/domain/participant/DealerTest.java b/src/test/java/domain/participant/DealerTest.java index e66ec55eaff..31c7d6899f5 100644 --- a/src/test/java/domain/participant/DealerTest.java +++ b/src/test/java/domain/participant/DealerTest.java @@ -3,39 +3,81 @@ import static domain.Fixture.ACE_SCORE; import static domain.Fixture.DEALER_HITTABLE_LOWER_BOUND_CARDS; import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.TWO_SCORE; import static domain.Fixture.createHandState; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Arrays; +import java.util.Collections; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import domain.participant.hand.HitState; -import domain.participant.hand.StayState; +import domain.card.CardDeck; +import domain.card.FakeCardDeck; class DealerTest { @DisplayName("constructor: 딜러 인스턴스 생성") @Test - void constructor() { + void constructor_NoArguments() { + assertThat(new Dealer()).isInstanceOf(Dealer.class); + } + + @DisplayName("constructor: 딜러 인스턴스 생성") + @Test + void constructor_OneArguments() { assertThat(new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS))).isInstanceOf(Dealer.class); } + @DisplayName("hitAtFirst: 최초로 카드를 두 장 뽑음") + @Test + void hitAtFirst() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Arrays.asList(ACE_SCORE, TWO_SCORE)); + + dealer.hitAtFirst(cardDeck); + + assertAll( + () -> assertThat(dealer.isBusted()).isFalse() + ); + } + @DisplayName("hit: 카드 한 장을 받고 16 이하이면 Hittable 상태 유지") @Test void hit_HittableState() { Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(ACE_SCORE)); - dealer.hit(ACE_SCORE); + dealer.hit(cardDeck); - assertThat(dealer.getHandState()).isInstanceOf(HitState.class); + assertAll( + () -> assertThat(dealer.isFinished()).isFalse(), + () -> assertThat(dealer.isBlackjack()).isFalse(), + () -> assertThat(dealer.isBusted()).isFalse() + ); } @DisplayName("hit: 카드 한 장을 받고 16 초과하면 stay 상태로 전이") @Test void hit_ChangeToStayState() { Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(ACE_SCORE)); + + dealer.hit(cardDeck); - dealer.hit(ACE_SCORE); + assertAll( + () -> assertThat(dealer.isFinished()).isTrue(), + () -> assertThat(dealer.isBlackjack()).isFalse(), + () -> assertThat(dealer.isBusted()).isFalse() + ); + } + + @DisplayName("getHand: hand를 반환") + @Test + void getHand() { + Dealer dealer = new Dealer(createHandState(DEALER_HITTABLE_LOWER_BOUND_CARDS)); - assertThat(dealer.getHandState()).isInstanceOf(StayState.class); + assertThat(dealer.getHand()).isNotNull(); } } diff --git a/src/test/java/domain/participant/PlayerDecisionTest.java b/src/test/java/domain/participant/PlayerDecisionTest.java new file mode 100644 index 00000000000..973ad359f89 --- /dev/null +++ b/src/test/java/domain/participant/PlayerDecisionTest.java @@ -0,0 +1,41 @@ +package domain.participant; + +import static domain.Fixture.EASTJUN; +import static domain.Fixture.HUNDRED_BETTING_MONEY; +import static domain.Fixture.TWO_SCORE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Collections; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import domain.card.CardDeck; +import domain.card.FakeCardDeck; + +class PlayerDecisionTest { + @DisplayName("of: PlayerDecision 반환") + @CsvSource(value = {"y, HIT", "n, STAY"}) + @ParameterizedTest + void of_ValidInput_ReturnPlayerDecision(final String input, final PlayerDecision expect) { + assertThat(PlayerDecision.of(input)).isEqualTo(expect); + } + + @DisplayName("of: PlayerDecision 반환") + @CsvSource(value = {"HIT, false", "STAY, true"}) + @ParameterizedTest + void act(final PlayerDecision playerDecision, final boolean isFinished) { + Player player = new Player(EASTJUN, HUNDRED_BETTING_MONEY); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TWO_SCORE)); + + playerDecision.act(cardDeck, player); + + assertAll( + () -> assertThat(player.isFinished()).isEqualTo(isFinished), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() + ); + } +} \ No newline at end of file diff --git a/src/test/java/domain/participant/PlayerTest.java b/src/test/java/domain/participant/PlayerTest.java index 61b209beafc..166e29ca58c 100644 --- a/src/test/java/domain/participant/PlayerTest.java +++ b/src/test/java/domain/participant/PlayerTest.java @@ -11,14 +11,14 @@ import static org.junit.jupiter.api.Assertions.assertAll; import java.math.BigDecimal; +import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import domain.participant.hand.BustedState; -import domain.participant.hand.HitState; -import domain.participant.hand.StayState; +import domain.card.CardDeck; +import domain.card.FakeCardDeck; class PlayerTest { private Player player; @@ -30,7 +30,13 @@ void setUp() { @DisplayName("constructor: 사용자 생성") @Test - void constructor() { + void constructor_TwoArguments() { + assertThat(new Player(EASTJUN, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); + } + + @DisplayName("constructor: 사용자 생성") + @Test + void constructor_ThreeArguments() { assertThat(new Player(EASTJUN, HITTABLE_HAND_STATE, HUNDRED_BETTING_MONEY)).isInstanceOf(Player.class); } @@ -61,20 +67,26 @@ void constructor_BettingMoneyIsNull_ExceptionThrown() { @DisplayName("hit: 카드를 한 장 뽑고 상태 전이") @Test void hit() { - player.hit(TWO_SCORE); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TWO_SCORE)); + player.hit(cardDeck); - assertThat(player.getHandState()).isInstanceOf(HitState.class); - assertThat(player.isFinished()).isFalse(); + assertAll( + () -> assertThat(player.isFinished()).isFalse(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() + ); } @DisplayName("hit: 카드를 한 장 뽑고 버스트 상태로 전이") @Test void hit_Busted() { - player.hit(TEN_SCORE); + CardDeck cardDeck = new FakeCardDeck(Collections.singletonList(TEN_SCORE)); + player.hit(cardDeck); assertAll( - () -> assertThat(player.getHandState()).isInstanceOf(BustedState.class), - () -> assertThat(player.isFinished()).isTrue() + () -> assertThat(player.isFinished()).isTrue(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isTrue() ); } @@ -84,12 +96,13 @@ void stay() { player.stay(); assertAll( - () -> assertThat(player.getHandState()).isInstanceOf(StayState.class), - () -> assertThat(player.isFinished()).isTrue() + () -> assertThat(player.isFinished()).isTrue(), + () -> assertThat(player.isBlackjack()).isFalse(), + () -> assertThat(player.isBusted()).isFalse() ); } - @DisplayName("수익률 계산") + @DisplayName("calculateProfit: 수익률 계산") @Test void calculateProfit() { player.stay(); @@ -99,6 +112,6 @@ void calculateProfit() { @DisplayName("getName: 이름을 반환") @Test void getName() { - assertThat(player.getName()).isEqualTo(EASTJUN); + assertThat(player.getName()).isEqualTo("eastjun"); } } diff --git a/src/test/java/domain/participant/hand/BlackjackStateTest.java b/src/test/java/domain/participant/hand/BlackjackStateTest.java index 862e1969ca1..4e9fe90118f 100644 --- a/src/test/java/domain/participant/hand/BlackjackStateTest.java +++ b/src/test/java/domain/participant/hand/BlackjackStateTest.java @@ -27,6 +27,12 @@ void draw() { assertThatThrownBy(() -> BLACKJACK_STATE.draw(TWO_SCORE)).isInstanceOf(UnsupportedOperationException.class); } + @DisplayName("isBusted: 버스트가 아님") + @Test + void isBusted() { + assertThat(BLACKJACK_STATE.isBusted()).isFalse(); + } + @DisplayName("isFinished: 끝났음을 반환") @Test void isFinished() { diff --git a/src/test/java/domain/participant/hand/HandTest.java b/src/test/java/domain/participant/hand/HandTest.java index 4a87d522018..3857697157b 100644 --- a/src/test/java/domain/participant/hand/HandTest.java +++ b/src/test/java/domain/participant/hand/HandTest.java @@ -1,11 +1,14 @@ package domain.participant.hand; +import static domain.Fixture.ACE_SCORE; import static domain.Fixture.BLACKJACK_CARDS; import static domain.Fixture.BUSTED_BY_ACE_CARDS; import static domain.Fixture.BUSTED_CARDS; import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; import static domain.Fixture.DEALER_NOT_HITTABLE_LOWER_BOUND_CARDS; import static domain.Fixture.MAX_SCORE_CARDS; +import static domain.Fixture.THREE_SCORE; +import static domain.Fixture.TWO_SCORE; import static domain.card.Face.ACE; import static domain.card.Suit.SPADE; import static org.assertj.core.api.Assertions.assertThat; @@ -105,4 +108,35 @@ void calculateScore(final List cards, final int expect) { assertThat(hand.calculateScore()).isEqualTo(expect); } + + @DisplayName("isInitialDraw: 두 장의 카드를 가지고 있으면 true 반환") + @Test + void isInitialDraw_HasTwoCards_ReturnTrue() { + Hand hand = new Hand(); + + hand.draw(ACE_SCORE); + hand.draw(TWO_SCORE); + + assertThat(hand.isInitialDraw()).isTrue(); + } + + @DisplayName("isInitialDraw: 두 장의 카드를 가지고 있으면 true 반환") + @Test + void isInitialDraw_NotHasTwoCards_ReturnFalse() { + Hand hand = new Hand(); + + hand.draw(ACE_SCORE); + hand.draw(TWO_SCORE); + hand.draw(THREE_SCORE); + + assertThat(hand.isInitialDraw()).isFalse(); + } + + @DisplayName("getCards: 카드 리스트를 반환") + @Test + void getCards() { + Hand hand = new Hand(); + + assertThat(hand.getCards()).isNotNull(); + } } diff --git a/src/test/java/domain/participant/hand/HitStateTest.java b/src/test/java/domain/participant/hand/HitStateTest.java index efd62f0a59d..57096793be0 100644 --- a/src/test/java/domain/participant/hand/HitStateTest.java +++ b/src/test/java/domain/participant/hand/HitStateTest.java @@ -1,8 +1,9 @@ package domain.participant.hand; -import static domain.Fixture.ACE_SCORE; import static domain.Fixture.DEALER_HITTABLE_UPPER_BOUND_CARDS; +import static domain.Fixture.FIVE_SCORE; import static domain.Fixture.FOUR_SCORE; +import static domain.Fixture.SIX_SCORE; import static domain.Fixture.createHand; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -10,6 +11,7 @@ import java.math.BigDecimal; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -19,16 +21,21 @@ import domain.card.Card; class HitStateTest { - private static final HitState HIT_STATE = new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + private HitState hitState; private static Stream createCardAndIsNextBust() { return Stream.of( Arguments.of(FOUR_SCORE, HitState.class), - Arguments.of(ACE_SCORE, HitState.class), - Arguments.of(FOUR_SCORE, BustedState.class) + Arguments.of(FIVE_SCORE, StayState.class), + Arguments.of(SIX_SCORE, BustedState.class) ); } + @BeforeEach + void setUp() { + hitState = new HitState(createHand(DEALER_HITTABLE_UPPER_BOUND_CARDS)); + } + @DisplayName("constructor: 초기 카드로 HitState 생성") @Test void constructor() { @@ -40,31 +47,31 @@ void constructor() { @MethodSource("createCardAndIsNextBust") @ParameterizedTest void draw(final Card card, final Class expect) { - assertThat(HIT_STATE.draw(card)).isInstanceOf(expect); + assertThat(hitState.draw(card)).isInstanceOf(expect); } @DisplayName("isFinished: 끝나지 않았음을 반환") @Test void isFinished() { - assertThat(HIT_STATE.isFinished()).isFalse(); + assertThat(hitState.isFinished()).isFalse(); } @DisplayName("stay: 턴을 마치고 상태 전이") @Test void stay() { - assertThat(HIT_STATE.stay()).isInstanceOf(StayState.class); + assertThat(hitState.stay()).isInstanceOf(StayState.class); } @DisplayName("calculateHand: 지원하지 않는 메서드를 호출하여 예외 발생") @Test void calculateHand() { - assertThatThrownBy(() -> HIT_STATE.calculateProfit(BigDecimal.valueOf(1_000))) + assertThatThrownBy(() -> hitState.calculateProfit(BigDecimal.valueOf(1_000))) .isInstanceOf(UnsupportedOperationException.class); } @DisplayName("getHand: 가지고 있는 패를 반환") @Test void getHand() { - assertThat(HIT_STATE.getHand()).isInstanceOf(Hand.class); + assertThat(hitState.getHand()).isInstanceOf(Hand.class); } } diff --git a/src/test/java/domain/participant/hand/ReadyStateTest.java b/src/test/java/domain/participant/hand/ReadyStateTest.java index 41c311327b2..3269360a83f 100644 --- a/src/test/java/domain/participant/hand/ReadyStateTest.java +++ b/src/test/java/domain/participant/hand/ReadyStateTest.java @@ -1,5 +1,7 @@ package domain.participant.hand; +import static domain.Fixture.ACE_SCORE; +import static domain.Fixture.TEN_SCORE; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; @@ -14,19 +16,29 @@ void setUp() { readyState = new ReadyState(); } - @DisplayName("Ready 상태 생성") + @DisplayName("constructor: Ready 상태 생성") @Test void constructor() { assertThat(new ReadyState()).isInstanceOf(ReadyState.class); } - @DisplayName("블랙잭이 아님을 확인") + @DisplayName("draw: 두 장 드로우하여 Blackjack 상태로 전이") + @Test + void draw() { + HandState handState = new ReadyState(); + handState = handState.draw(ACE_SCORE); + handState = handState.draw(TEN_SCORE); + + assertThat(handState.isBlackjack()).isTrue(); + } + + @DisplayName("isBlackjack: 블랙잭이 아님을 확인") @Test void isBlackjack() { assertThat(readyState.isBlackjack()).isFalse(); } - @DisplayName("버스트가 아님을 확인") + @DisplayName("isBusted: 버스트가 아님을 확인") @Test void isBusted() { assertThat(readyState.isBusted()).isFalse(); diff --git a/src/test/java/domain/result/ParticipantProfitTest.java b/src/test/java/domain/result/ParticipantProfitTest.java new file mode 100644 index 00000000000..b7e56b47a4c --- /dev/null +++ b/src/test/java/domain/result/ParticipantProfitTest.java @@ -0,0 +1,43 @@ +package domain.result; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import domain.participant.Dealer; +import domain.participant.Participant; + +public class ParticipantProfitTest { + @DisplayName("constructor: 인스턴스 생성") + @Test + void constructor() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + + assertThat(new ParticipantProfit(participant, profit)).isInstanceOf(ParticipantProfit.class); + } + + @DisplayName("getName: name을 반환") + @Test + void getName() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + ParticipantProfit participantProfit = new ParticipantProfit(participant, profit); + + assertThat(participantProfit.getName()).isNotNull(); + } + + @DisplayName("getProfit: profit을 반환") + @Test + void getProfit() { + Participant participant = new Dealer(); + BigDecimal profit = BigDecimal.ZERO; + + ParticipantProfit participantProfit = new ParticipantProfit(participant, profit); + + assertThat(participantProfit.getProfit()).isNotNull(); + } +} From 83bc73db28a9087004c92cbd89627e55b29b32ce Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Thu, 2 Jul 2020 22:40:26 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=8F=20=EC=9E=85=EC=B6=9C=EB=A0=A5=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BlackjackApplication.java | 39 +++++++++ src/main/java/controller/GameController.java | 87 ++++++++++++++++++++ src/main/java/view/InputView.java | 32 +++++++ src/main/java/view/OutputView.java | 72 ++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 src/main/java/BlackjackApplication.java create mode 100644 src/main/java/controller/GameController.java create mode 100644 src/main/java/view/InputView.java create mode 100644 src/main/java/view/OutputView.java diff --git a/src/main/java/BlackjackApplication.java b/src/main/java/BlackjackApplication.java new file mode 100644 index 00000000000..50ab147aac5 --- /dev/null +++ b/src/main/java/BlackjackApplication.java @@ -0,0 +1,39 @@ +import java.util.Scanner; + +import controller.GameController; +import service.DrawService; +import service.PlayerService; +import service.ProfitService; +import view.InputView; +import view.OutputView; + +public class BlackjackApplication { + private final GameController gameController; + + public BlackjackApplication(final GameController gameController) { + this.gameController = gameController; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + InputView inputView = new InputView(scanner); + OutputView outputView = new OutputView(); + PlayerService playerService = new PlayerService(); + DrawService drawService = new DrawService(); + ProfitService profitService = new ProfitService(); + + GameController gameController = new GameController(inputView, outputView, playerService, drawService, + profitService); + BlackjackApplication blackjackApplication = new BlackjackApplication(gameController); + + blackjackApplication.run(); + } + + public void run() { + try { + gameController.run(); + } catch (Exception exception) { + System.err.println(exception.getMessage()); + } + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000000..32871a70465 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,87 @@ +package controller; + +import java.util.ArrayList; +import java.util.List; + +import domain.BettingMoney; +import domain.Name; +import domain.card.CardDeck; +import domain.card.CardDeckFactory; +import domain.participant.Dealer; +import domain.participant.Player; +import domain.result.ParticipantProfit; +import service.DrawService; +import service.PlayerService; +import service.ProfitService; +import view.InputView; +import view.OutputView; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final PlayerService playerService; + private final DrawService drawService; + private final ProfitService profitService; + + public GameController(final InputView inputView, final OutputView outputView, final PlayerService playerService, + final DrawService drawService, final ProfitService profitService) { + this.inputView = inputView; + this.outputView = outputView; + this.playerService = playerService; + this.drawService = drawService; + this.profitService = profitService; + } + + public void run() { + CardDeck cardDeck = CardDeckFactory.createRandomCardDeck(); + Dealer dealer = new Dealer(); + List players = createPlayers(); + + drawCards(cardDeck, dealer, players); + + calculateProfit(dealer, players); + } + + private List createPlayers() { + String namesWithComma = inputView.inputNames(); + List names = Name.fromComma(namesWithComma); + List bettingMonies = new ArrayList<>(); + for (final Name name : names) { + int money = inputView.inputBettingMoney(name); + BettingMoney bettingMoney = new BettingMoney(money); + bettingMonies.add(bettingMoney); + } + return playerService.createPlayers(names, bettingMonies); + } + + private void drawCards(final CardDeck cardDeck, final Dealer dealer, final List players) { + drawService.drawInitialCards(cardDeck, dealer, players); + outputView.printHandsAtFirst(dealer, players); + + for (final Player player : players) { + drawCardToPlayer(cardDeck, player); + } + drawCardToDealer(cardDeck, dealer); + } + + private void calculateProfit(final Dealer dealer, final List players) { + outputView.printHandsAtLast(dealer, players); + List participantProfits = profitService.calculateProfits(dealer, players); + outputView.printProfits(participantProfits); + } + + private void drawCardToDealer(final CardDeck cardDeck, final Dealer dealer) { + while (!dealer.isFinished()) { + drawService.drawDealer(cardDeck, dealer); + outputView.printDealerHitMessage(); + } + } + + private void drawCardToPlayer(final CardDeck cardDeck, final Player player) { + while (!player.isFinished()) { + String decision = inputView.inputPlayerDecision(player); + drawService.drawPlayer(cardDeck, player, decision); + outputView.printHand(player); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000000..fcb6f2dc15f --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,32 @@ +package view; + +import java.util.Scanner; + +import domain.Name; +import domain.participant.Player; + +public class InputView { + private final Scanner scanner; + + public InputView(final Scanner scanner) { + this.scanner = scanner; + } + + public String inputNames() { + System.out.println(); + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + return scanner.nextLine(); + } + + public int inputBettingMoney(final Name name) { + System.out.println(); + System.out.printf("%s의 배팅 금액은?\n", name.getName()); + return Integer.parseInt(scanner.nextLine()); + } + + public String inputPlayerDecision(final Player player) { + System.out.println(); + System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)\n", player.getName()); + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..c4d589653da --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,72 @@ +package view; + +import java.io.PrintStream; +import java.util.List; +import java.util.stream.Collectors; + +import domain.card.Card; +import domain.participant.Dealer; +import domain.participant.Participant; +import domain.participant.Player; +import domain.participant.hand.Hand; +import domain.result.ParticipantProfit; + +public class OutputView { + + private static final String DELIMITER = ", "; + + public void printHandsAtFirst(final Dealer dealer, final List players) { + System.out.println(); + System.out.printf("%s와 %s에게 2장의 나누었습니다.\n", dealer.getName(), renderNames(players)); + + System.out.printf("딜러카드: %s\n", dealer.getHand().getCards().get(1).alias()); + players.forEach(this::printHand); + } + + public void printHand(final Participant participant) { + System.out.println(renderHand(participant)); + } + + public void printDealerHitMessage() { + System.out.println(); + System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다."); + } + + public void printHandsAtLast(final Dealer dealer, final List players) { + System.out.println(); + printHandAtLast(dealer); + players.forEach(this::printHandAtLast); + } + + public void printProfits(final List participantProfits) { + System.out.println(); + System.out.println("## 최종 수익"); + participantProfits.forEach(this::printProfit); + } + + private void printHandAtLast(final Participant participant) { + System.out.printf("%s - 결과: %d\n", renderHand(participant), participant.getHand().calculateScore()); + } + + private String renderHand(final Participant participant) { + return String.format("%s카드: %s", participant.getName(), renderCards(participant.getHand())); + } + + private String renderNames(final List players) { + return players.stream() + .map(Participant::getName) + .collect(Collectors.joining(DELIMITER)); + } + + private String renderCards(final Hand hand) { + return hand.getCards() + .stream() + .map(Card::alias) + .collect(Collectors.joining(DELIMITER)); + } + + private PrintStream printProfit(final ParticipantProfit participantProfit) { + return System.out.printf("%s: %d\n", participantProfit.getName(), + participantProfit.getProfit().longValueExact()); + } +} From 954927533a6db1527dc2b712c5be491824d6d333 Mon Sep 17 00:00:00 2001 From: "K.S.KIM" Date: Thu, 2 Jul 2020 22:57:06 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=A1=B0=EC=9E=91=20=EB=AA=BB=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/domain/participant/hand/Hand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/domain/participant/hand/Hand.java b/src/main/java/domain/participant/hand/Hand.java index 568d50e02e2..2b9c64c0a5a 100644 --- a/src/main/java/domain/participant/hand/Hand.java +++ b/src/main/java/domain/participant/hand/Hand.java @@ -1,6 +1,7 @@ package domain.participant.hand; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import domain.card.Card; @@ -69,6 +70,6 @@ public boolean isInitialDraw() { } public List getCards() { - return cards; + return Collections.unmodifiableList(cards); } }