From 3f269ccc0e98d18d9047262ea03425f952545a69 Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 10:09:04 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix(step1):=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/kotlin/study/DslTest.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/test/kotlin/study/DslTest.kt b/src/test/kotlin/study/DslTest.kt index d9cf4cc01f..3231707af8 100644 --- a/src/test/kotlin/study/DslTest.kt +++ b/src/test/kotlin/study/DslTest.kt @@ -5,21 +5,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.ValueSource -/** - * introduce { - * name("홍길동") - * company("활빈당") - * skills { - * soft("A passion for problem solving") - * soft("Good communication skills") - * hard("Kotiln") - * } - * languages { - * "Korean" level 5 - * "English" level 3 - * } - * } - */ class DslTest { // @ParameterizedTest @ValueSource(strings = ["홍길동", "허균"]) From ec50971c7eaf5c48dac4032b42e5ef6f895bb11b Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 10:13:59 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix(step2):=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/.gitkeep | 0 src/test/kotlin/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/kotlin/.gitkeep delete mode 100644 src/test/kotlin/.gitkeep diff --git a/src/main/kotlin/.gitkeep b/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/kotlin/.gitkeep b/src/test/kotlin/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 From 3574bae880f98395527969b394bd267e1d4825b9 Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 11:59:59 +0900 Subject: [PATCH 03/10] =?UTF-8?q?docs(step2):=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1c7c927d8..0894669033 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack + +## 기능 요구사항 +블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. + + - 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. + - 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. + +## 프로그래밍 요구 사항 + - 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외 + - indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + - 모든 엔티티를 작게 유지한다. + - 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. + - 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다. + - git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다. + +## 구현할 기능 목록 +### 점수 + - [ ] 숫자 카드는 카드의 숫자를 점수로 계산한다 + - [ ] Ace 카드는 1 또는 11로 점수 계산 가능하다 + - [ ] King, Queen, Jack은 10으로 계산한다 + +### 시작 + - [ ] 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 고유한 이름이 있다) + +### 카드 분배 + - [ ] 플레이어는 최초에 카드 2장을 분배 받는다 + +### 카드 추가 + - [ ] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있다 (21 이상이면 추가로 카드를 받을 수 없다) + +### 결과 + - [ ] 최종 점수는 보유한 카드의 점수를 합한 값이다 + - [ ] 최종 점수가 블랙잭 기준치(21)를 넘으면 버스트로 점수와 상관없이 패배이다 + - [ ] Ace 카드는 가능한 한 버스트 되지 않고 가장 큰 점수를 받을 수 있도록 계산한다 From 3fe467d3343fa53cba71eeb03150a2f231722b1d Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 13:27:51 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(step2):=20'=EC=A0=90=EC=88=98'=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 숫자 카드는 카드의 숫자를 점수로 계산한다 - Ace 카드는 1 또는 11로 점수 계산 가능하다 - King, Queen, Jack은 10으로 계산한다 --- README.md | 6 +- src/main/kotlin/blackjack/domain/Card.kt | 61 ++++++++++++++++++++ src/test/kotlin/blackjack/domain/CardTest.kt | 55 ++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/blackjack/domain/Card.kt create mode 100644 src/test/kotlin/blackjack/domain/CardTest.kt diff --git a/README.md b/README.md index 0894669033..1d9c1c42e7 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ ## 구현할 기능 목록 ### 점수 - - [ ] 숫자 카드는 카드의 숫자를 점수로 계산한다 - - [ ] Ace 카드는 1 또는 11로 점수 계산 가능하다 - - [ ] King, Queen, Jack은 10으로 계산한다 + - [x] 숫자 카드는 카드의 숫자를 점수로 계산한다 + - [x] Ace 카드는 1 또는 11로 점수 계산 가능하다 + - [x] King, Queen, Jack은 10으로 계산한다 ### 시작 - [ ] 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 고유한 이름이 있다) diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt new file mode 100644 index 0000000000..ce2abd24e9 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Card.kt @@ -0,0 +1,61 @@ +package blackjack.domain + +data class Card(val suit: Suit, val rank: Rank, val possibleScore: PossibleScore) { + companion object { + + private val cardPool: Set = + Suit.values().flatMap { suit -> + Rank.values().map { rank -> + Card(suit = suit, rank = rank, possibleScore = PossibleScore.getPossibleScore(rank)) + } + }.toSet() + + fun getDeck(): Deck = cardPool.toMutableSet() + } +} + +typealias Deck = MutableSet + +enum class Suit { + HEART, + DIAMOND, + SPADE, + CLUB; +} + +enum class Rank(val number: Int) { + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(11), + QUEEN(12), + KING(13); +} + +@JvmInline +value class PossibleScore(val values: Set) { + companion object { + fun getPossibleScore(rank: Rank) = when (rank) { + Rank.ACE -> setOf(1, 11) + Rank.TWO -> setOf(2) + Rank.THREE -> setOf(3) + Rank.FOUR -> setOf(4) + Rank.FIVE -> setOf(5) + Rank.SIX -> setOf(6) + Rank.SEVEN -> setOf(7) + Rank.EIGHT -> setOf(8) + Rank.NINE -> setOf(9) + Rank.TEN -> setOf(10) + Rank.JACK -> setOf(10) + Rank.QUEEN -> setOf(10) + Rank.KING -> setOf(10) + }.let { PossibleScore(it) } + } +} diff --git a/src/test/kotlin/blackjack/domain/CardTest.kt b/src/test/kotlin/blackjack/domain/CardTest.kt new file mode 100644 index 0000000000..5cb3f13947 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardTest.kt @@ -0,0 +1,55 @@ +package blackjack.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class CardTest { + @ParameterizedTest + @MethodSource("provideAceCards") + fun `Ace 카드는 1 또는 11로 점수 계산 가능하다`(sameRankCards: List) { + val expectedPossibleScore = PossibleScore(setOf(1, 11)) + sameRankCards.forEach { card -> + assertEquals(expectedPossibleScore, card.possibleScore) + } + } + + @ParameterizedTest + @MethodSource("provideNumberCards") + fun `숫자 카드는 카드의 숫자를 점수로 계산한다`(sameRankCards: List) { + val expectedPossibleScore = PossibleScore(setOf(sameRankCards[0].rank.number)) + sameRankCards.forEach { card -> + assertEquals(expectedPossibleScore, card.possibleScore) + } + } + + @ParameterizedTest + @MethodSource("provideCourtCards") + fun `King, Queen, Jack은 10으로 계산한다`(sameRankCards: List) { + val expectedPossibleScore = PossibleScore(setOf(10)) + sameRankCards.forEach { card -> + assertEquals(expectedPossibleScore, card.possibleScore) + } + } + + companion object { + @JvmStatic + fun provideAceCards() = + listOf(Rank.ACE) + .map { Card.getDeck().filter { card -> card.rank == it } } + .map { Arguments.of(it) } + + @JvmStatic + fun provideNumberCards() = + listOf(Rank.TWO, Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN, Rank.EIGHT, Rank.NINE, Rank.TEN) + .map { Card.getDeck().filter { card -> card.rank == it } } + .map { Arguments.of(it) } + + @JvmStatic + fun provideCourtCards() = + listOf(Rank.JACK, Rank.QUEEN, Rank.KING) + .map { Card.getDeck().filter { card -> card.rank == it } } + .map { Arguments.of(it) } + } +} From b55865265ec391606e80a33e83153205d780eb77 Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 16:15:15 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(step2):=20'=EC=8B=9C=EC=9E=91'=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입력한 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 유일한 이름을 가진다) --- README.md | 2 +- src/main/kotlin/blackjack/domain/Player.kt | 14 ++++++++++++++ src/test/kotlin/blackjack/domain/PlayerTest.kt | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/blackjack/domain/Player.kt create mode 100644 src/test/kotlin/blackjack/domain/PlayerTest.kt diff --git a/README.md b/README.md index 1d9c1c42e7..aa328235d2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ - [x] King, Queen, Jack은 10으로 계산한다 ### 시작 - - [ ] 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 고유한 이름이 있다) + - [x] 입력한 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 유일한 이름을 가진다) ### 카드 분배 - [ ] 플레이어는 최초에 카드 2장을 분배 받는다 diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt new file mode 100644 index 0000000000..697cfbed54 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -0,0 +1,14 @@ +package blackjack.domain + +data class Player(val name: String) + +@JvmInline +value class Players(val values: List) { + + constructor(vararg names: String): this(names.map { Player(it) }) + + init { + require(values.size == values.map { it.name }.toSet().size) { "duplicate name has been used" } + } +} + diff --git a/src/test/kotlin/blackjack/domain/PlayerTest.kt b/src/test/kotlin/blackjack/domain/PlayerTest.kt new file mode 100644 index 0000000000..8196228d66 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/PlayerTest.kt @@ -0,0 +1,17 @@ +package blackjack.domain + +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class PlayerTest { + @ParameterizedTest + @ValueSource(strings = ["a,a", "a,b,a", "a,b,b"]) + fun `플레이어는 유일한 이름을 가진다`(namesInput: String) { + // given + val names = namesInput.split(",").toTypedArray() + + // when, then + assertThrows { Players(*names) } + } +} From 6cdd3de575b6395e6d04e6cd69b87e7313296fd8 Mon Sep 17 00:00:00 2001 From: david-sang Date: Mon, 27 Nov 2023 16:15:59 +0900 Subject: [PATCH 06/10] style(step2): ktlint --- src/main/kotlin/blackjack/domain/Player.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index 697cfbed54..ded4034a52 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -5,10 +5,9 @@ data class Player(val name: String) @JvmInline value class Players(val values: List) { - constructor(vararg names: String): this(names.map { Player(it) }) + constructor(vararg names: String) : this(names.map { Player(it) }) init { require(values.size == values.map { it.name }.toSet().size) { "duplicate name has been used" } } } - From acda12df4f93a3dfe3039e69fa4002f4a39dfaf4 Mon Sep 17 00:00:00 2001 From: david-sang Date: Thu, 30 Nov 2023 17:19:07 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat(step2):=20'=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=B0=B0'=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 플레이어는 최초에 카드 2장을 분배 받는다 - 카드 분배는 최초 한 번 이루어진다 --- README.md | 3 +- src/main/kotlin/blackjack/domain/Card.kt | 52 +++++++++---------- src/main/kotlin/blackjack/domain/Deck.kt | 23 ++++++++ src/main/kotlin/blackjack/domain/Hand.kt | 27 ++++++++++ src/main/kotlin/blackjack/domain/Player.kt | 47 ++++++++++++++++- src/main/kotlin/blackjack/domain/Rule.kt | 7 +++ src/main/kotlin/blackjack/domain/Score.kt | 16 ++++++ src/test/kotlin/blackjack/domain/CardTest.kt | 50 +++++++++--------- .../kotlin/blackjack/domain/PlayerTest.kt | 21 ++++++-- 9 files changed, 188 insertions(+), 58 deletions(-) create mode 100644 src/main/kotlin/blackjack/domain/Deck.kt create mode 100644 src/main/kotlin/blackjack/domain/Hand.kt create mode 100644 src/main/kotlin/blackjack/domain/Rule.kt create mode 100644 src/main/kotlin/blackjack/domain/Score.kt diff --git a/README.md b/README.md index aa328235d2..7a321318f8 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ - [x] 입력한 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 유일한 이름을 가진다) ### 카드 분배 - - [ ] 플레이어는 최초에 카드 2장을 분배 받는다 + - [x] 플레이어는 최초에 카드 2장을 분배 받는다 + - [x] 카드 분배는 최초 한 번 이루어진다 ### 카드 추가 - [ ] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있다 (21 이상이면 추가로 카드를 받을 수 없다) diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt index ce2abd24e9..a64c086dab 100644 --- a/src/main/kotlin/blackjack/domain/Card.kt +++ b/src/main/kotlin/blackjack/domain/Card.kt @@ -1,21 +1,11 @@ package blackjack.domain -data class Card(val suit: Suit, val rank: Rank, val possibleScore: PossibleScore) { - companion object { - - private val cardPool: Set = - Suit.values().flatMap { suit -> - Rank.values().map { rank -> - Card(suit = suit, rank = rank, possibleScore = PossibleScore.getPossibleScore(rank)) - } - }.toSet() +data class Card(val suit: Suit, val rank: Rank) { + val possibleScore: PossibleScore = PossibleScore.getPossibleScore(rank) - fun getDeck(): Deck = cardPool.toMutableSet() - } + fun getPossibleScoreSums(score: Score): List = possibleScore.getPossibleScoreSums(score) } -typealias Deck = MutableSet - enum class Suit { HEART, DIAMOND, @@ -40,22 +30,28 @@ enum class Rank(val number: Int) { } @JvmInline -value class PossibleScore(val values: Set) { +value class PossibleScore(private val values: Set) { + + fun getPossibleScoreSums(score: Score): List = values.map { score + it } + companion object { fun getPossibleScore(rank: Rank) = when (rank) { - Rank.ACE -> setOf(1, 11) - Rank.TWO -> setOf(2) - Rank.THREE -> setOf(3) - Rank.FOUR -> setOf(4) - Rank.FIVE -> setOf(5) - Rank.SIX -> setOf(6) - Rank.SEVEN -> setOf(7) - Rank.EIGHT -> setOf(8) - Rank.NINE -> setOf(9) - Rank.TEN -> setOf(10) - Rank.JACK -> setOf(10) - Rank.QUEEN -> setOf(10) - Rank.KING -> setOf(10) - }.let { PossibleScore(it) } + Rank.ACE -> listOf(1, 11) + Rank.TWO -> listOf(2) + Rank.THREE -> listOf(3) + Rank.FOUR -> listOf(4) + Rank.FIVE -> listOf(5) + Rank.SIX -> listOf(6) + Rank.SEVEN -> listOf(7) + Rank.EIGHT -> listOf(8) + Rank.NINE -> listOf(9) + Rank.TEN -> listOf(10) + Rank.JACK -> listOf(10) + Rank.QUEEN -> listOf(10) + Rank.KING -> listOf(10) + } + .map(Score::from) + .toSet() + .let(::PossibleScore) } } diff --git a/src/main/kotlin/blackjack/domain/Deck.kt b/src/main/kotlin/blackjack/domain/Deck.kt new file mode 100644 index 0000000000..359bbc3d0e --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Deck.kt @@ -0,0 +1,23 @@ +package blackjack.domain + +@JvmInline +value class Deck(private val cards: MutableSet) { + fun draw(): Card { + check(cards.isNotEmpty()) { "no cards left" } + val card = cards.random() + cards.remove(card) + return card + } + + companion object { + private val preparedDeck: Set = + Suit.values().flatMap { suit -> + Rank.values().map { rank -> + Card(suit = suit, rank = rank) + } + }.toSet() + + fun getDeck(): Deck = preparedDeck.toMutableSet().let(::Deck) + } + +} diff --git a/src/main/kotlin/blackjack/domain/Hand.kt b/src/main/kotlin/blackjack/domain/Hand.kt new file mode 100644 index 0000000000..9c7357a28a --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Hand.kt @@ -0,0 +1,27 @@ +package blackjack.domain + +@JvmInline +value class Hand(private val cards: MutableList = mutableListOf()) { + + fun add(card: Card) = cards.add(card) + + fun getCardCount() = cards.size + + fun getBestScore(): Score { + val scoreSums = mutableSetOf() + calculateScoreSums(cards.toList(), 0, Score.from(0), scoreSums) + return scoreSums + .filter { it.value <= Rule.BLACKJACK_SCORE } + .maxBy { it.value } + } + + private fun calculateScoreSums(cards: List, index: Int, sum: Score, sums: MutableSet) { + if(index == cards.size) { + sums.add(sum) + return + } + cards[index].getPossibleScoreSums(sum).forEach { + calculateScoreSums(cards, index + 1, it, sums) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index ded4034a52..0caeea96ec 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -1,9 +1,44 @@ package blackjack.domain -data class Player(val name: String) +data class Player(val name: String) { + var state: PlayerState = PlayerState.READY + private set + + val hand: Hand = Hand() + + fun init(deck: Deck) { + check(state == PlayerState.READY) { "can only 'init' if the 'PlayerState' is 'READY'" } + repeat(Rule.INIT_CARD_COUNT) { hand.add(deck.draw()) } + state = PlayerState.UNDER + } + + fun hit(deck: Deck) { + check(state == PlayerState.UNDER) { "can only 'hit' if the 'PlayerState' is 'UNDER'" } + hand.add(deck.draw()) + updateState() + } + + fun stay() { + check(state == PlayerState.UNDER) { "can only 'stay' if the 'PlayerState' is 'UNDER'" } + state = PlayerState.STAY + } + + private fun updateState() { + val score = hand.getBestScore() + val count = hand.getCardCount() + state = when { + score.value > Rule.BLACKJACK_SCORE -> PlayerState.BUST + score.value == Rule.BLACKJACK_SCORE -> when (count == Rule.BLACKJACK_CARD_COUNT) { + true -> PlayerState.BLACKJACK + false -> PlayerState.STAY + } + else -> state + } + } +} @JvmInline -value class Players(val values: List) { +value class Players(private val values: List) { constructor(vararg names: String) : this(names.map { Player(it) }) @@ -11,3 +46,11 @@ value class Players(val values: List) { require(values.size == values.map { it.name }.toSet().size) { "duplicate name has been used" } } } + +enum class PlayerState { + READY, + UNDER, + STAY, + BLACKJACK, + BUST; +} diff --git a/src/main/kotlin/blackjack/domain/Rule.kt b/src/main/kotlin/blackjack/domain/Rule.kt new file mode 100644 index 0000000000..e851ba65b9 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Rule.kt @@ -0,0 +1,7 @@ +package blackjack.domain + +object Rule { + const val BLACKJACK_SCORE = 21 + const val BLACKJACK_CARD_COUNT = 2 + const val INIT_CARD_COUNT = 2 +} diff --git a/src/main/kotlin/blackjack/domain/Score.kt b/src/main/kotlin/blackjack/domain/Score.kt new file mode 100644 index 0000000000..b256467c88 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Score.kt @@ -0,0 +1,16 @@ +package blackjack.domain + + +@JvmInline +value class Score private constructor(val value: Int) { + + operator fun plus(score: Score) = from(value + score.value) + + companion object { + private const val MIN_SCORE = 0 + private const val MAX_SCORE = 100 + private val preparedScores = (MIN_SCORE..MAX_SCORE).associateWith(::Score) + + fun from(value: Int): Score = preparedScores[value] ?: Score(value) + } +} diff --git a/src/test/kotlin/blackjack/domain/CardTest.kt b/src/test/kotlin/blackjack/domain/CardTest.kt index 5cb3f13947..13271ac101 100644 --- a/src/test/kotlin/blackjack/domain/CardTest.kt +++ b/src/test/kotlin/blackjack/domain/CardTest.kt @@ -8,48 +8,50 @@ import org.junit.jupiter.params.provider.MethodSource class CardTest { @ParameterizedTest @MethodSource("provideAceCards") - fun `Ace 카드는 1 또는 11로 점수 계산 가능하다`(sameRankCards: List) { - val expectedPossibleScore = PossibleScore(setOf(1, 11)) - sameRankCards.forEach { card -> - assertEquals(expectedPossibleScore, card.possibleScore) - } + fun `Ace 카드는 1 또는 11로 점수 계산 가능하다`(card: Card) { + val expectedPossibleScore = PossibleScore(setOf(Score.from(1), Score.from(11))) + assertEquals(expectedPossibleScore, card.possibleScore) } @ParameterizedTest @MethodSource("provideNumberCards") - fun `숫자 카드는 카드의 숫자를 점수로 계산한다`(sameRankCards: List) { - val expectedPossibleScore = PossibleScore(setOf(sameRankCards[0].rank.number)) - sameRankCards.forEach { card -> - assertEquals(expectedPossibleScore, card.possibleScore) - } + fun `숫자 카드는 카드의 숫자를 점수로 계산한다`(card: Card) { + val expectedPossibleScore = PossibleScore(setOf(Score.from(card.rank.number))) + assertEquals(expectedPossibleScore, card.possibleScore) } @ParameterizedTest @MethodSource("provideCourtCards") - fun `King, Queen, Jack은 10으로 계산한다`(sameRankCards: List) { - val expectedPossibleScore = PossibleScore(setOf(10)) - sameRankCards.forEach { card -> - assertEquals(expectedPossibleScore, card.possibleScore) - } + fun `King, Queen, Jack은 10으로 계산한다`(card: Card) { + val expectedPossibleScore = PossibleScore(setOf(Score.from(10))) + assertEquals(expectedPossibleScore, card.possibleScore) } companion object { @JvmStatic - fun provideAceCards() = + fun provideAceCards() = makeCards( + Suit.values().toList(), listOf(Rank.ACE) - .map { Card.getDeck().filter { card -> card.rank == it } } - .map { Arguments.of(it) } + ).map { Arguments.of(it) } @JvmStatic - fun provideNumberCards() = + fun provideNumberCards() = makeCards( + Suit.values().toList(), listOf(Rank.TWO, Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN, Rank.EIGHT, Rank.NINE, Rank.TEN) - .map { Card.getDeck().filter { card -> card.rank == it } } - .map { Arguments.of(it) } + ).map { Arguments.of(it) } @JvmStatic - fun provideCourtCards() = + fun provideCourtCards() = makeCards( + Suit.values().toList(), listOf(Rank.JACK, Rank.QUEEN, Rank.KING) - .map { Card.getDeck().filter { card -> card.rank == it } } - .map { Arguments.of(it) } + ).map { Arguments.of(it) } + + private fun makeCards(suits: List, ranks: List): List { + return suits.flatMap { suit -> + ranks.map { rank -> + Card(suit, rank) + } + } + } } } diff --git a/src/test/kotlin/blackjack/domain/PlayerTest.kt b/src/test/kotlin/blackjack/domain/PlayerTest.kt index 8196228d66..fd038d4b89 100644 --- a/src/test/kotlin/blackjack/domain/PlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/PlayerTest.kt @@ -1,5 +1,7 @@ package blackjack.domain +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -8,10 +10,23 @@ class PlayerTest { @ParameterizedTest @ValueSource(strings = ["a,a", "a,b,a", "a,b,b"]) fun `플레이어는 유일한 이름을 가진다`(namesInput: String) { - // given val names = namesInput.split(",").toTypedArray() - - // when, then assertThrows { Players(*names) } } + + @Test + fun `플레이어는 최초에 카드 2장을 분배 받는다`() { + val player: Player = Player("test") + val deck: Deck = Deck.getDeck() + player.init(deck) + assertEquals(Rule.INIT_CARD_COUNT, player.hand.getCardCount()) + } + + @Test + fun `카드 분배는 최초 한 번만 이루어진다`() { + val player: Player = Player("test") + val deck: Deck = Deck.getDeck() + player.init(deck) + assertThrows { player.init(deck) } + } } From d2a058e1d3d5b8b3d6b12b6b02556d11a5f63cd3 Mon Sep 17 00:00:00 2001 From: david-sang Date: Thu, 30 Nov 2023 17:34:33 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat(step2):=20'=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80'=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있는 상태이다 --- README.md | 2 +- src/main/kotlin/blackjack/domain/Player.kt | 8 +++----- src/test/kotlin/blackjack/domain/PlayerTest.kt | 12 ++++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7a321318f8..76c54b14b0 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [x] 카드 분배는 최초 한 번 이루어진다 ### 카드 추가 - - [ ] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있다 (21 이상이면 추가로 카드를 받을 수 없다) + - [x] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있는 상태이다 ### 결과 - [ ] 최종 점수는 보유한 카드의 점수를 합한 값이다 diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index 0caeea96ec..7e9e1cbe78 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -1,11 +1,9 @@ package blackjack.domain -data class Player(val name: String) { +data class Player(val name: String, val hand: Hand = Hand()) { var state: PlayerState = PlayerState.READY private set - val hand: Hand = Hand() - fun init(deck: Deck) { check(state == PlayerState.READY) { "can only 'init' if the 'PlayerState' is 'READY'" } repeat(Rule.INIT_CARD_COUNT) { hand.add(deck.draw()) } @@ -23,7 +21,7 @@ data class Player(val name: String) { state = PlayerState.STAY } - private fun updateState() { + fun updateState() { val score = hand.getBestScore() val count = hand.getCardCount() state = when { @@ -32,7 +30,7 @@ data class Player(val name: String) { true -> PlayerState.BLACKJACK false -> PlayerState.STAY } - else -> state + else -> PlayerState.UNDER } } } diff --git a/src/test/kotlin/blackjack/domain/PlayerTest.kt b/src/test/kotlin/blackjack/domain/PlayerTest.kt index fd038d4b89..0b19715940 100644 --- a/src/test/kotlin/blackjack/domain/PlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/PlayerTest.kt @@ -29,4 +29,16 @@ class PlayerTest { player.init(deck) assertThrows { player.init(deck) } } + + @Test + fun `보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있는 상태이다`() { + val cards: MutableList = mutableListOf( + Card(Suit.HEART, Rank.TEN), + Card(Suit.CLUB, Rank.TEN) + ) + val hand: Hand = Hand(cards) + val player: Player = Player("test", hand) + player.updateState() + assertEquals(PlayerState.UNDER, player.state) + } } From d66fe54fd6c19e866a5773ca19118c81085a0126 Mon Sep 17 00:00:00 2001 From: david-sang Date: Thu, 30 Nov 2023 20:40:42 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat(step2):=20'=EA=B2=B0=EA=B3=BC'=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 만들 수 있는 최종 점수가 모두 블랙잭 기준치(21)를 넘는다면 버스트이다과 - 버스트인 경우, 최종 점수는 모든 카드를 더해서 만들 수 있는 가장 작은 수이다 - 버스트가 아닌 경우, 최종 점수는 모든 카드를 더해서 블랙잭 기준치(21) 이하에서 만들 수 있는 가장 큰 수이다 - 최종 점수가 블랙잭 기준치(21)이고 가진 카드가 2장이면 블랙잭이다 --- README.md | 7 ++- src/main/kotlin/blackjack/domain/Deck.kt | 1 - src/main/kotlin/blackjack/domain/Hand.kt | 15 +++-- src/main/kotlin/blackjack/domain/Player.kt | 11 +++- src/test/kotlin/blackjack/domain/HandTest.kt | 63 +++++++++++++++++++ .../kotlin/blackjack/domain/PlayerTest.kt | 25 ++++++++ 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/test/kotlin/blackjack/domain/HandTest.kt diff --git a/README.md b/README.md index 76c54b14b0..381ea94c52 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ - [x] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있는 상태이다 ### 결과 - - [ ] 최종 점수는 보유한 카드의 점수를 합한 값이다 - - [ ] 최종 점수가 블랙잭 기준치(21)를 넘으면 버스트로 점수와 상관없이 패배이다 - - [ ] Ace 카드는 가능한 한 버스트 되지 않고 가장 큰 점수를 받을 수 있도록 계산한다 + - [x] 만들 수 있는 최종 점수가 모두 블랙잭 기준치(21)를 넘는다면 버스트이다과 + - [x] 버스트인 경우, 최종 점수는 모든 카드를 더해서 만들 수 있는 가장 작은 수이다 + - [x] 버스트가 아닌 경우, 최종 점수는 모든 카드를 더해서 블랙잭 기준치(21) 이하에서 만들 수 있는 가장 큰 수이다 + - [x] 최종 점수가 블랙잭 기준치(21)이고 가진 카드가 2장이면 블랙잭이다 diff --git a/src/main/kotlin/blackjack/domain/Deck.kt b/src/main/kotlin/blackjack/domain/Deck.kt index 359bbc3d0e..fec6e35ab1 100644 --- a/src/main/kotlin/blackjack/domain/Deck.kt +++ b/src/main/kotlin/blackjack/domain/Deck.kt @@ -19,5 +19,4 @@ value class Deck(private val cards: MutableSet) { fun getDeck(): Deck = preparedDeck.toMutableSet().let(::Deck) } - } diff --git a/src/main/kotlin/blackjack/domain/Hand.kt b/src/main/kotlin/blackjack/domain/Hand.kt index 9c7357a28a..3e97872714 100644 --- a/src/main/kotlin/blackjack/domain/Hand.kt +++ b/src/main/kotlin/blackjack/domain/Hand.kt @@ -10,13 +10,20 @@ value class Hand(private val cards: MutableList = mutableListOf()) { fun getBestScore(): Score { val scoreSums = mutableSetOf() calculateScoreSums(cards.toList(), 0, Score.from(0), scoreSums) - return scoreSums - .filter { it.value <= Rule.BLACKJACK_SCORE } - .maxBy { it.value } + return when (scoreSums.minBy { it.value }.value <= Rule.BLACKJACK_SCORE) { + true -> { + scoreSums + .filter { it.value <= Rule.BLACKJACK_SCORE } + .maxBy { it.value } + } + false -> { + scoreSums.minBy { it.value } + } + } } private fun calculateScoreSums(cards: List, index: Int, sum: Score, sums: MutableSet) { - if(index == cards.size) { + if (index == cards.size) { sums.add(sum) return } diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index 7e9e1cbe78..abfd4ced07 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -4,6 +4,8 @@ data class Player(val name: String, val hand: Hand = Hand()) { var state: PlayerState = PlayerState.READY private set + val resultScore: Score by lazy { decideScore() } + fun init(deck: Deck) { check(state == PlayerState.READY) { "can only 'init' if the 'PlayerState' is 'READY'" } repeat(Rule.INIT_CARD_COUNT) { hand.add(deck.draw()) } @@ -25,14 +27,19 @@ data class Player(val name: String, val hand: Hand = Hand()) { val score = hand.getBestScore() val count = hand.getCardCount() state = when { + score.value < Rule.BLACKJACK_SCORE -> PlayerState.UNDER score.value > Rule.BLACKJACK_SCORE -> PlayerState.BUST - score.value == Rule.BLACKJACK_SCORE -> when (count == Rule.BLACKJACK_CARD_COUNT) { + else -> when (count == Rule.BLACKJACK_CARD_COUNT) { true -> PlayerState.BLACKJACK false -> PlayerState.STAY } - else -> PlayerState.UNDER } } + + private fun decideScore(): Score { + check(state == PlayerState.STAY || state == PlayerState.BLACKJACK || state == PlayerState.BUST) { "can't decide score until the action is over" } + return hand.getBestScore() + } } @JvmInline diff --git a/src/test/kotlin/blackjack/domain/HandTest.kt b/src/test/kotlin/blackjack/domain/HandTest.kt new file mode 100644 index 0000000000..a2a4083a32 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/HandTest.kt @@ -0,0 +1,63 @@ +package blackjack.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class HandTest { + + @ParameterizedTest + @MethodSource("provideBustScore") + fun `버스트인 경우, 최종 점수는 모든 카드를 더해서 만들 수 있는 가장 작은 수이다`(cards: MutableList, expectedScoreValue: Int) { + val hand = cards.let(::Hand) + val expectedScore = Score.from(expectedScoreValue) + assertEquals(expectedScore, hand.getBestScore()) + } + + @ParameterizedTest + @MethodSource("provideBestScore") + fun `버스트가 아닌 경우, 최종 점수는 모든 카드를 더해서 블랙잭 기준치(21) 이하에서 만들 수 있는 가장 큰 수이다`( + cards: MutableList, + expectedScoreValue: Int + ) { + val hand = cards.let(::Hand) + val expectedScore = Score.from(expectedScoreValue) + assertEquals(expectedScore, hand.getBestScore()) + } + + companion object { + @JvmStatic + fun provideBustScore() = listOf( + Arguments.of( + mutableListOf( + Card(Suit.HEART, Rank.JACK), + Card(Suit.HEART, Rank.QUEEN), + Card(Suit.HEART, Rank.ACE), + Card(Suit.CLUB, Rank.TWO) + ), + 23 + ), + Arguments.of( + mutableListOf(Card(Suit.HEART, Rank.JACK), Card(Suit.HEART, Rank.QUEEN), Card(Suit.CLUB, Rank.TWO)), + 22 + ), + ) + + @JvmStatic + fun provideBestScore() = listOf( + Arguments.of( + mutableListOf(Card(Suit.CLUB, Rank.ACE), Card(Suit.CLUB, Rank.NINE)), + 20 + ), + Arguments.of( + mutableListOf(Card(Suit.CLUB, Rank.ACE), Card(Suit.CLUB, Rank.ACE)), + 12 + ), + Arguments.of( + mutableListOf(Card(Suit.CLUB, Rank.ACE), Card(Suit.CLUB, Rank.JACK)), + 21 + ), + ) + } +} diff --git a/src/test/kotlin/blackjack/domain/PlayerTest.kt b/src/test/kotlin/blackjack/domain/PlayerTest.kt index 0b19715940..d63ade41dd 100644 --- a/src/test/kotlin/blackjack/domain/PlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/PlayerTest.kt @@ -41,4 +41,29 @@ class PlayerTest { player.updateState() assertEquals(PlayerState.UNDER, player.state) } + + @Test + fun `만들 수 있는 최종 점수가 모두 블랙잭 기준치(21)를 넘는다면 버스트이다`() { + val cards: MutableList = mutableListOf( + Card(Suit.HEART, Rank.TEN), + Card(Suit.CLUB, Rank.TEN), + Card(Suit.CLUB, Rank.TWO), + ) + val hand: Hand = Hand(cards) + val player: Player = Player("test", hand) + player.updateState() + assertEquals(PlayerState.BUST, player.state) + } + + @Test + fun `최종 점수가 블랙잭 기준치(21)이고 가진 카드가 2장이면 블랙잭이다`() { + val cards: MutableList = mutableListOf( + Card(Suit.HEART, Rank.TEN), + Card(Suit.CLUB, Rank.ACE), + ) + val hand: Hand = Hand(cards) + val player: Player = Player("test", hand) + player.updateState() + assertEquals(PlayerState.BLACKJACK, player.state) + } } From 356b1ea91cd0b6ada41b6be271adf0b28a71fea7 Mon Sep 17 00:00:00 2001 From: david-sang Date: Fri, 1 Dec 2023 19:57:55 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat(step2):=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - controller - view --- src/main/kotlin/blackjack/Main.kt | 10 ++++ .../controller/BlackjackController.kt | 38 ++++++++++++++ src/main/kotlin/blackjack/domain/Game.kt | 32 ++++++++++++ src/main/kotlin/blackjack/domain/Hand.kt | 2 +- src/main/kotlin/blackjack/domain/Phase.kt | 13 +++++ src/main/kotlin/blackjack/domain/Player.kt | 22 +++++--- src/main/kotlin/blackjack/view/InputView.kt | 13 +++++ src/main/kotlin/blackjack/view/OutputView.kt | 50 +++++++++++++++++++ 8 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/blackjack/Main.kt create mode 100644 src/main/kotlin/blackjack/controller/BlackjackController.kt create mode 100644 src/main/kotlin/blackjack/domain/Game.kt create mode 100644 src/main/kotlin/blackjack/domain/Phase.kt create mode 100644 src/main/kotlin/blackjack/view/InputView.kt create mode 100644 src/main/kotlin/blackjack/view/OutputView.kt diff --git a/src/main/kotlin/blackjack/Main.kt b/src/main/kotlin/blackjack/Main.kt new file mode 100644 index 0000000000..c5a5ecb8bb --- /dev/null +++ b/src/main/kotlin/blackjack/Main.kt @@ -0,0 +1,10 @@ +package blackjack + +import blackjack.controller.BlackjackController + +fun main() { + val controller = BlackjackController() + val game = controller.startGame() + repeat(game.players.values.size) { controller.progressPlayerPhase(game) } + controller.progressEndPhase(game) +} diff --git a/src/main/kotlin/blackjack/controller/BlackjackController.kt b/src/main/kotlin/blackjack/controller/BlackjackController.kt new file mode 100644 index 0000000000..1874d73924 --- /dev/null +++ b/src/main/kotlin/blackjack/controller/BlackjackController.kt @@ -0,0 +1,38 @@ +package blackjack.controller + +import blackjack.domain.Game +import blackjack.domain.Phase +import blackjack.view.InputView +import blackjack.view.OutputView + +class BlackjackController { + fun startGame(): Game { + val names = InputView.getNames() + val game = Game(names) + game.players.values.forEach { + OutputView.printGameInitialization(game) + OutputView.printPlayerAndCard(it) + } + return game + } + + fun progressPlayerPhase(game: Game) { + val phase = game.checkAndGetPhase(Phase.PlayerPhase::class) + while (!phase.isFinish()) { + when (InputView.getHitOrStay(phase.player.name)) { + "y" -> phase.player.hit(game.deck) + "n" -> phase.player.stay() + else -> throw IllegalArgumentException("only 'y' or 'n' can be entered") + } + OutputView.printPlayerAndCard(phase.player) + } + game.moveToNextPhase() + } + + fun progressEndPhase(game: Game) { + val phase = game.checkAndGetPhase(Phase.EndPhase::class) + phase.players.values.forEach { + OutputView.printPlayerAndCardAndScore(it) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Game.kt b/src/main/kotlin/blackjack/domain/Game.kt new file mode 100644 index 0000000000..36b1a1aad7 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Game.kt @@ -0,0 +1,32 @@ +package blackjack.domain + +import kotlin.reflect.KClass + +class Game(val players: Players, val deck: Deck = Deck.getDeck()) { + + private val phases: List + private var phaseIndex: Int = 0 + private val phase + get() = phases[phaseIndex] + + constructor(names: List) : this(names.map(::Player).let(::Players)) + + init { + players.values.forEach { + it.initialize(deck) + } + phases = players.values.map(Phase::PlayerPhase) + players.let(Phase::EndPhase) + } + + fun moveToNextPhase() { + check(phase.isFinish()) { "current phase is not over" } + check(phaseIndex < phases.size) { "there is no more next phase" } + phaseIndex++ + } + + fun checkAndGetPhase(phaseType: KClass): T { + check(phaseType.isInstance(phase)) { "not ${phaseType::class.java.name}" } + return phase as T + } + +} diff --git a/src/main/kotlin/blackjack/domain/Hand.kt b/src/main/kotlin/blackjack/domain/Hand.kt index 3e97872714..cf535c503e 100644 --- a/src/main/kotlin/blackjack/domain/Hand.kt +++ b/src/main/kotlin/blackjack/domain/Hand.kt @@ -1,7 +1,7 @@ package blackjack.domain @JvmInline -value class Hand(private val cards: MutableList = mutableListOf()) { +value class Hand(val cards: MutableList = mutableListOf()) { fun add(card: Card) = cards.add(card) diff --git a/src/main/kotlin/blackjack/domain/Phase.kt b/src/main/kotlin/blackjack/domain/Phase.kt new file mode 100644 index 0000000000..86f76b4835 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Phase.kt @@ -0,0 +1,13 @@ +package blackjack.domain + +sealed interface Phase { + fun isFinish(): Boolean + + class PlayerPhase(val player: Player) : Phase { + override fun isFinish(): Boolean = player.resolved() + } + + class EndPhase(val players: Players) : Phase { + override fun isFinish(): Boolean = true + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index abfd4ced07..e9b78a3886 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -6,21 +6,21 @@ data class Player(val name: String, val hand: Hand = Hand()) { val resultScore: Score by lazy { decideScore() } - fun init(deck: Deck) { + fun initialize(deck: Deck) { check(state == PlayerState.READY) { "can only 'init' if the 'PlayerState' is 'READY'" } repeat(Rule.INIT_CARD_COUNT) { hand.add(deck.draw()) } state = PlayerState.UNDER } fun hit(deck: Deck) { - check(state == PlayerState.UNDER) { "can only 'hit' if the 'PlayerState' is 'UNDER'" } + check(active()) { "can only 'hit' if the 'PlayerState' is 'UNDER'" } hand.add(deck.draw()) updateState() } fun stay() { - check(state == PlayerState.UNDER) { "can only 'stay' if the 'PlayerState' is 'UNDER'" } - state = PlayerState.STAY + check(active()) { "can only 'stay' if the 'PlayerState' is 'UNDER'" } + state = PlayerState.STAND } fun updateState() { @@ -31,19 +31,25 @@ data class Player(val name: String, val hand: Hand = Hand()) { score.value > Rule.BLACKJACK_SCORE -> PlayerState.BUST else -> when (count == Rule.BLACKJACK_CARD_COUNT) { true -> PlayerState.BLACKJACK - false -> PlayerState.STAY + false -> PlayerState.STAND } } } + fun ready() = state == PlayerState.READY + + fun active() = state == PlayerState.UNDER + + fun resolved() = state == PlayerState.STAND || state == PlayerState.BLACKJACK || state == PlayerState.BUST + private fun decideScore(): Score { - check(state == PlayerState.STAY || state == PlayerState.BLACKJACK || state == PlayerState.BUST) { "can't decide score until the action is over" } + check(resolved()) { "can't decide score until the action is over" } return hand.getBestScore() } } @JvmInline -value class Players(private val values: List) { +value class Players(val values: List) { constructor(vararg names: String) : this(names.map { Player(it) }) @@ -55,7 +61,7 @@ value class Players(private val values: List) { enum class PlayerState { READY, UNDER, - STAY, + STAND, BLACKJACK, BUST; } diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt new file mode 100644 index 0000000000..dea410a33f --- /dev/null +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -0,0 +1,13 @@ +package blackjack.view + +object InputView { + fun getNames(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + return readln().split(",") + } + + fun getHitOrStay(name: String): String { + println("${name}은(는) 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + return readln() + } +} diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt new file mode 100644 index 0000000000..70c8fda6ad --- /dev/null +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -0,0 +1,50 @@ +package blackjack.view + +import blackjack.domain.Game +import blackjack.domain.Hand +import blackjack.domain.Player +import blackjack.domain.Rank +import blackjack.domain.Suit + +object OutputView { + fun printGameInitialization(game: Game) { + println("${game.players.values.joinToString(separator = ", ") { it.name }}에게 2장씩 나누었습니다.") + game.players.values.forEach { + printPlayerAndCard(it) + } + } + + fun printPlayerAndCard(player: Player) { + println("${player.name}카드: ${player.hand.makeForm()}") + } + + fun printPlayerAndCardAndScore(player: Player) { + println("${player.name}카드: ${player.hand.makeForm()} - 결과: ${player.resultScore.value}") + } + + private fun Hand.makeForm() = + this.cards.joinToString(separator = ", ") { "${it.rank.makeForm()}${it.suit.makeForm()}" } + + private fun Rank.makeForm() = when (this) { + Rank.ACE -> "A" + Rank.TWO -> "2" + Rank.THREE -> "3" + Rank.FOUR -> "4" + Rank.FIVE -> "5" + Rank.SIX -> "6" + Rank.SEVEN -> "7" + Rank.EIGHT -> "8" + Rank.NINE -> "9" + Rank.TEN -> "10" + Rank.JACK -> "J" + Rank.QUEEN -> "Q" + Rank.KING -> "K" + } + + private fun Suit.makeForm() = when (this) { + Suit.CLUB -> "클로버" + Suit.DIAMOND -> "다이아몬드" + Suit.HEART -> "하트" + Suit.SPADE -> "스페이드" + } +}