From b3acbfb8c13ca6935bd65fa7d2dd54fd9e8d496e Mon Sep 17 00:00:00 2001 From: bmsk Date: Fri, 24 Nov 2023 13:49:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[Feat]=20=EB=B8=94=EB=9E=99=EC=9E=AD?= =?UTF-8?q?=EC=97=90=20=EB=94=9C=EB=9F=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BLACKJACK.md | 8 ++- .../src/main/kotlin/action/BlackJackAction.kt | 6 +++ .../kotlin/blackjack/BlackjackParticipant.kt | 10 ++++ .../main/kotlin/blackjack/dealer/Dealer.kt | 24 +++++++++ .../kotlin/blackjack/dealer/DealerStrategy.kt | 9 ++++ .../blackjack/dealer/DefaultDealerStrategy.kt | 34 +++++++++++++ domain/src/main/kotlin/blackjack/deck/Deck.kt | 3 ++ .../blackjack/deck/RandomCardShuffler.kt | 2 +- .../blackjack/deck/StandardCardProvider.kt | 2 +- domain/src/main/kotlin/blackjack/hand/Hand.kt | 36 ++----------- .../kotlin/blackjack/hand/StandardHand.kt | 50 +++++++++++++++++++ .../main/kotlin/blackjack/player/Player.kt | 21 +++++--- .../kotlin/blackjack/dealer/DealerTest.kt | 37 ++++++++++++++ .../test/kotlin/blackjack/hand/HandTest.kt | 8 +-- .../kotlin/blackjack/player/PlayerTest.kt | 12 ++--- presenter/src/main/kotlin/ui/Main.kt | 6 +-- 16 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 domain/src/main/kotlin/action/BlackJackAction.kt create mode 100644 domain/src/main/kotlin/blackjack/BlackjackParticipant.kt create mode 100644 domain/src/main/kotlin/blackjack/dealer/Dealer.kt create mode 100644 domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt create mode 100644 domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt create mode 100644 domain/src/main/kotlin/blackjack/hand/StandardHand.kt create mode 100644 domain/src/test/kotlin/blackjack/dealer/DealerTest.kt diff --git a/docs/BLACKJACK.md b/docs/BLACKJACK.md index 76812cc101..0b5206a5f4 100644 --- a/docs/BLACKJACK.md +++ b/docs/BLACKJACK.md @@ -55,4 +55,10 @@ jason카드: 7클로버, K스페이드 - 결과: 17 - [ ] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다. - [ ] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다. -- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다. \ No newline at end of file +- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다. + +## 딜러가 추가된 사전 구현 계획 + +- [ ] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. + - [x] 만일 딜러가 을 갖고 있다면? + - [x] 딜러는 다양한 전략을 사용할 수 있도록 한다. \ No newline at end of file diff --git a/domain/src/main/kotlin/action/BlackJackAction.kt b/domain/src/main/kotlin/action/BlackJackAction.kt new file mode 100644 index 0000000000..7fc6b271d6 --- /dev/null +++ b/domain/src/main/kotlin/action/BlackJackAction.kt @@ -0,0 +1,6 @@ +package action + +enum class BlackJackAction { + HIT, + STAND, +} diff --git a/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt new file mode 100644 index 0000000000..5096cf05b5 --- /dev/null +++ b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt @@ -0,0 +1,10 @@ +package blackjack + +import blackjack.card.Card +import blackjack.hand.Hand + +interface BlackjackParticipant { + val hand: Hand + fun receiveCard(card: Card): BlackjackParticipant + fun calculateBestValue(): Int +} diff --git a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt new file mode 100644 index 0000000000..22dc0eff7c --- /dev/null +++ b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt @@ -0,0 +1,24 @@ +package blackjack.dealer + +import action.BlackJackAction +import blackjack.BlackjackParticipant +import blackjack.card.Card +import blackjack.deck.Deck +import blackjack.hand.Hand + +data class Dealer( + override val hand: Hand, + private val dealerStrategy: DealerStrategy = DefaultDealerStrategy() +) : BlackjackParticipant { + + val cards: List + get() = hand.cards.toList() + + override fun receiveCard(card: Card): Dealer = copy(hand = hand.addCard(card)) + + override fun calculateBestValue(): Int = hand.calculateBestValue() + + fun decideAction(deck: Deck): BlackJackAction { + return dealerStrategy.decideAction(hand, deck) + } +} diff --git a/domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt b/domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt new file mode 100644 index 0000000000..016af3f4ef --- /dev/null +++ b/domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt @@ -0,0 +1,9 @@ +package blackjack.dealer + +import action.BlackJackAction +import blackjack.deck.Deck +import blackjack.hand.Hand + +interface DealerStrategy { + fun decideAction(hand: Hand, deck: Deck): BlackJackAction +} diff --git a/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt new file mode 100644 index 0000000000..9bb80351ce --- /dev/null +++ b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt @@ -0,0 +1,34 @@ +package blackjack.dealer + +import action.BlackJackAction +import blackjack.card.CardRank +import blackjack.deck.Deck +import blackjack.hand.Hand + +internal class DefaultDealerStrategy : DealerStrategy { + override fun decideAction(hand: Hand, deck: Deck): BlackJackAction { + val dealerScore = hand.calculateBestValue() + val dealerMinScore = hand.calculateMinValue() + + val bustingProbability = maxOf( + calculateProbabilityOfBusting(dealerScore, deck), + calculateProbabilityOfBusting(dealerMinScore, deck) + ) + + return if (bustingProbability > 0.5) BlackJackAction.STAND else BlackJackAction.HIT + } + + private fun calculateProbabilityOfBusting(currentScore: Int, deck: Deck): Double { + val scoreNeededToAvoidBust = 21 - currentScore + val safeCards = deck.remainingCards.count { card -> + val cardValue = when (card.rank) { + CardRank.KING, CardRank.QUEEN, CardRank.JACK -> 10 + CardRank.ACE -> 11 + else -> card.rank.ordinal + 1 + } + cardValue <= scoreNeededToAvoidBust + } + + return 1.0 - safeCards.toDouble() / deck.size.toDouble() + } +} diff --git a/domain/src/main/kotlin/blackjack/deck/Deck.kt b/domain/src/main/kotlin/blackjack/deck/Deck.kt index d3ae3bc5f3..f9cc2bd4a9 100644 --- a/domain/src/main/kotlin/blackjack/deck/Deck.kt +++ b/domain/src/main/kotlin/blackjack/deck/Deck.kt @@ -11,6 +11,9 @@ class Deck( addAll(cardShuffler.shuffle(cardProvider.provideCards())) } + val remainingCards: List + get() = cards.toList() + val size get() = cards.size diff --git a/domain/src/main/kotlin/blackjack/deck/RandomCardShuffler.kt b/domain/src/main/kotlin/blackjack/deck/RandomCardShuffler.kt index 40a69d5e6a..981d9ab18f 100644 --- a/domain/src/main/kotlin/blackjack/deck/RandomCardShuffler.kt +++ b/domain/src/main/kotlin/blackjack/deck/RandomCardShuffler.kt @@ -2,6 +2,6 @@ package blackjack.deck import blackjack.card.Card -class RandomCardShuffler : CardShuffler { +internal class RandomCardShuffler : CardShuffler { override fun shuffle(cards: List): List = cards.shuffled() } diff --git a/domain/src/main/kotlin/blackjack/deck/StandardCardProvider.kt b/domain/src/main/kotlin/blackjack/deck/StandardCardProvider.kt index ba77587382..1939a173e0 100644 --- a/domain/src/main/kotlin/blackjack/deck/StandardCardProvider.kt +++ b/domain/src/main/kotlin/blackjack/deck/StandardCardProvider.kt @@ -4,7 +4,7 @@ import blackjack.card.Card import blackjack.card.CardRank import blackjack.card.CardSuit -class StandardCardProvider : CardProvider { +internal class StandardCardProvider : CardProvider { override fun provideCards(): List = CardSuit.values().flatMap { suit -> CardRank.values().map { rank -> Card(suit, rank) } diff --git a/domain/src/main/kotlin/blackjack/hand/Hand.kt b/domain/src/main/kotlin/blackjack/hand/Hand.kt index 3c57027b42..285f4f0574 100644 --- a/domain/src/main/kotlin/blackjack/hand/Hand.kt +++ b/domain/src/main/kotlin/blackjack/hand/Hand.kt @@ -1,36 +1,10 @@ package blackjack.hand import blackjack.card.Card -import blackjack.card.CardRank -data class Hand( - val cards: List = emptyList() -) { - fun addCard(card: Card): Hand = copy(cards = cards + card) - - fun calculateBestValue(): Int { - val sumWithoutAces = cards.filter { it.rank != CardRank.ACE }.sumOf { cardValue(it) } - val aceCount = cards.count { it.rank == CardRank.ACE } - return calculateBestAceValue(sumWithoutAces, aceCount) - } - - private fun cardValue(card: Card): Int = when (card.rank) { - CardRank.KING, CardRank.QUEEN, CardRank.JACK -> FACE_CARD_VALUE - else -> card.rank.ordinal + 1 - } - - private fun calculateBestAceValue(sumWithoutAces: Int, aceCount: Int): Int { - var sum = sumWithoutAces - repeat(aceCount) { - sum += if (sum + ACE_HIGH_VALUE > MAX_HAND_VALUE) ACE_LOW_VALUE else ACE_HIGH_VALUE - } - return sum - } - - companion object { - private const val FACE_CARD_VALUE = 10 - private const val MAX_HAND_VALUE = 21 - private const val ACE_HIGH_VALUE = 11 - private const val ACE_LOW_VALUE = 1 - } +interface Hand { + val cards: Set + fun addCard(card: Card): Hand + fun calculateMinValue(): Int + fun calculateBestValue(): Int } diff --git a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt new file mode 100644 index 0000000000..5b73f0b867 --- /dev/null +++ b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt @@ -0,0 +1,50 @@ +package blackjack.hand + +import blackjack.card.Card +import blackjack.card.CardRank + +internal class StandardHand( + override val cards: Set = emptySet() +) : Hand { + override fun addCard(card: Card): StandardHand = StandardHand(cards = cards + card) + + override fun calculateBestValue(): Int { + val sumWithoutAces = cards.filter { it.rank != CardRank.ACE }.sumOf { cardValue(it) } + val aceCount = cards.count { it.rank == CardRank.ACE } + return calculateBestAceValue(sumWithoutAces, aceCount) + } + + override fun calculateMinValue(): Int { + val sumWithoutAces = cards.filter { it.rank != CardRank.ACE }.sumOf { cardValue(it) } + val aceCount = cards.count { it.rank == CardRank.ACE } + return calculateMinAceValue(sumWithoutAces, aceCount) + } + + private fun cardValue(card: Card): Int = when (card.rank) { + CardRank.KING, CardRank.QUEEN, CardRank.JACK -> FACE_CARD_VALUE + else -> card.rank.ordinal + 1 + } + + private fun calculateBestAceValue(sumWithoutAces: Int, aceCount: Int): Int { + var sum = sumWithoutAces + repeat(aceCount) { + sum += if (sum + ACE_HIGH_VALUE > MAX_HAND_VALUE) ACE_LOW_VALUE else ACE_HIGH_VALUE + } + return sum + } + + private fun calculateMinAceValue(sumWithoutAces: Int, aceCount: Int): Int { + var sum = sumWithoutAces + repeat(aceCount) { + sum += ACE_LOW_VALUE + } + return sum + } + + companion object { + private const val FACE_CARD_VALUE = 10 + private const val MAX_HAND_VALUE = 21 + private const val ACE_HIGH_VALUE = 11 + private const val ACE_LOW_VALUE = 1 + } +} diff --git a/domain/src/main/kotlin/blackjack/player/Player.kt b/domain/src/main/kotlin/blackjack/player/Player.kt index 0aa382d713..c8b752fd9f 100644 --- a/domain/src/main/kotlin/blackjack/player/Player.kt +++ b/domain/src/main/kotlin/blackjack/player/Player.kt @@ -1,18 +1,27 @@ package blackjack.player +import action.BlackJackAction +import blackjack.BlackjackParticipant import blackjack.card.Card import blackjack.hand.Hand data class Player( val name: String, - private val hand: Hand, -) { + override val hand: Hand, +) : BlackjackParticipant { + val cards: List - get() = hand.cards + get() = hand.cards.toList() + + fun canHit(): BlackJackAction = if (hand.calculateMinValue() <= 21) { + BlackJackAction.HIT + } else { + BlackJackAction.STAND + } - fun canReceiveCard(): Boolean = hand.calculateBestValue() <= 21 - fun receiveCard(card: Card): Player { + override fun receiveCard(card: Card): Player { return copy(hand = hand.addCard(card)) } - fun calculateBestValue(): Int = hand.calculateBestValue() + + override fun calculateBestValue(): Int = hand.calculateBestValue() } diff --git a/domain/src/test/kotlin/blackjack/dealer/DealerTest.kt b/domain/src/test/kotlin/blackjack/dealer/DealerTest.kt new file mode 100644 index 0000000000..f4d6f3cd32 --- /dev/null +++ b/domain/src/test/kotlin/blackjack/dealer/DealerTest.kt @@ -0,0 +1,37 @@ +package blackjack.dealer + +import action.BlackJackAction +import blackjack.card.Card +import blackjack.card.CardRank +import blackjack.card.CardSuit +import blackjack.deck.Deck +import blackjack.hand.StandardHand +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class DealerTest : FunSpec({ + + test("처음 딜러의 손패 수는 0이다.") { + val dealer = Dealer(hand = StandardHand()) + dealer.cards.size shouldBe 0 + } + + test("손패의 수가 0일 때 결정할 액션은 HIT이다") { + val dealer = Dealer(hand = StandardHand()) + dealer.decideAction(deck = Deck()) shouldBe BlackJackAction.HIT + } + + test("딜러는 카드를 받으면 손패의 수가 1 증가한다.") { + Dealer(hand = StandardHand()).also { + it.cards.size shouldBe 0 + }.receiveCard(card = Card(suit = CardSuit.CLUBS, rank = CardRank.ACE)) + .cards.size shouldBe 1 + } + + test("ACE와 JACK을 가지고 있을 때, 베스트는 21이다.") { + Dealer(hand = StandardHand()) + .receiveCard(card = Card(suit = CardSuit.CLUBS, rank = CardRank.ACE)) + .receiveCard(card = Card(suit = CardSuit.DIAMONDS, rank = CardRank.JACK)) + .calculateBestValue() shouldBe 21 + } +}) diff --git a/domain/src/test/kotlin/blackjack/hand/HandTest.kt b/domain/src/test/kotlin/blackjack/hand/HandTest.kt index 5cda518013..32c161f2b8 100644 --- a/domain/src/test/kotlin/blackjack/hand/HandTest.kt +++ b/domain/src/test/kotlin/blackjack/hand/HandTest.kt @@ -8,8 +8,8 @@ import io.kotest.matchers.shouldBe class HandTest : FunSpec({ test("손패가 Ace, 10으로 이뤄져 있다면 21로 계산한다.") { - val hand = Hand( - cards = listOf( + val hand = StandardHand( + cards = setOf( Card(suit = CardSuit.CLUBS, rank = CardRank.ACE), Card(suit = CardSuit.HEARTS, rank = CardRank.TEN) ) @@ -19,8 +19,8 @@ class HandTest : FunSpec({ } test("손패가 Ace, 10, 2으로 이뤄져 있다면 13으로 계산한다.") { - val hand = Hand( - cards = listOf( + val hand = StandardHand( + cards = setOf( Card(suit = CardSuit.CLUBS, rank = CardRank.ACE), Card(suit = CardSuit.HEARTS, rank = CardRank.TEN), Card(suit = CardSuit.HEARTS, rank = CardRank.TWO), diff --git a/domain/src/test/kotlin/blackjack/player/PlayerTest.kt b/domain/src/test/kotlin/blackjack/player/PlayerTest.kt index e659735123..762fda39ef 100644 --- a/domain/src/test/kotlin/blackjack/player/PlayerTest.kt +++ b/domain/src/test/kotlin/blackjack/player/PlayerTest.kt @@ -1,30 +1,30 @@ package blackjack.player +import action.BlackJackAction import blackjack.card.Card import blackjack.card.CardRank import blackjack.card.CardSuit -import blackjack.hand.Hand +import blackjack.hand.StandardHand import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe class PlayerTest : StringSpec({ "플레이어는 핸드의 최고 값이 21 이하일 때 카드를 뽑을 수 있어야 한다" { - val hand = Hand(listOf(Card(CardSuit.SPADES, CardRank.ACE))) + val hand = StandardHand(setOf(Card(CardSuit.SPADES, CardRank.ACE))) val player = Player("테스터", hand) - player.canReceiveCard().shouldBeTrue() + player.canHit() shouldBe BlackJackAction.HIT } "플레이어가 카드를 뽑을 때 플레이어의 핸드가 업데이트 되어야 한다" { - val player = Player("테스터", Hand()) + val player = Player("테스터", StandardHand()) player.cards.size shouldBe 0 val newPlayer = player.receiveCard(card = Card(CardSuit.SPADES, CardRank.ACE)) newPlayer.cards.size shouldBe 1 } "플레이어의 최고 값 계산이 올바르게 수행되어야 한다: A스페이드 + 10다이아는 21이다." { - val hand = Hand(listOf(Card(CardSuit.SPADES, CardRank.ACE), Card(CardSuit.DIAMONDS, CardRank.TEN))) + val hand = StandardHand(setOf(Card(CardSuit.SPADES, CardRank.ACE), Card(CardSuit.DIAMONDS, CardRank.TEN))) val player = Player("테스터", hand) player.calculateBestValue() shouldBe 21 } diff --git a/presenter/src/main/kotlin/ui/Main.kt b/presenter/src/main/kotlin/ui/Main.kt index fac17ff9ca..80b2e8f89c 100644 --- a/presenter/src/main/kotlin/ui/Main.kt +++ b/presenter/src/main/kotlin/ui/Main.kt @@ -1,7 +1,7 @@ package ui import blackjack.deck.Deck -import blackjack.hand.Hand +import blackjack.hand.StandardHand import blackjack.player.Player import ui.input.InputView import ui.result.ResultView @@ -12,7 +12,7 @@ fun main() { val playerNames = inputView.inputPlayerNames() val deck = Deck() - val players = playerNames.map { Player(it, Hand()) } + val players = playerNames.map { Player(it, StandardHand()) } playBlackjack(deck, players, inputView, resultView) } @@ -29,7 +29,7 @@ fun playBlackjack(deck: Deck, players: List, inputView: InputView, resul } fun handlePlayerTurn(deck: Deck, player: Player, inputView: InputView, resultView: ResultView) { - while (player.canReceiveCard()) { + while (player.decideAction()) { if (!inputView.askForAdditionalCard(player.name)) { return } From bc8dc6f2c7c845b990acd6850e09c299fe995e97 Mon Sep 17 00:00:00 2001 From: bmsk Date: Fri, 24 Nov 2023 13:54:01 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[Fix]=20Presenter=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/kotlin/blackjack/player/Player.kt | 3 ++- presenter/src/main/kotlin/ui/Main.kt | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/domain/src/main/kotlin/blackjack/player/Player.kt b/domain/src/main/kotlin/blackjack/player/Player.kt index c8b752fd9f..1575c37a33 100644 --- a/domain/src/main/kotlin/blackjack/player/Player.kt +++ b/domain/src/main/kotlin/blackjack/player/Player.kt @@ -4,10 +4,11 @@ import action.BlackJackAction import blackjack.BlackjackParticipant import blackjack.card.Card import blackjack.hand.Hand +import blackjack.hand.StandardHand data class Player( val name: String, - override val hand: Hand, + override val hand: Hand = StandardHand(), ) : BlackjackParticipant { val cards: List diff --git a/presenter/src/main/kotlin/ui/Main.kt b/presenter/src/main/kotlin/ui/Main.kt index 80b2e8f89c..fb25b71ce9 100644 --- a/presenter/src/main/kotlin/ui/Main.kt +++ b/presenter/src/main/kotlin/ui/Main.kt @@ -1,7 +1,7 @@ package ui +import action.BlackJackAction import blackjack.deck.Deck -import blackjack.hand.StandardHand import blackjack.player.Player import ui.input.InputView import ui.result.ResultView @@ -12,7 +12,7 @@ fun main() { val playerNames = inputView.inputPlayerNames() val deck = Deck() - val players = playerNames.map { Player(it, StandardHand()) } + val players = playerNames.map { Player(it) } playBlackjack(deck, players, inputView, resultView) } @@ -29,11 +29,11 @@ fun playBlackjack(deck: Deck, players: List, inputView: InputView, resul } fun handlePlayerTurn(deck: Deck, player: Player, inputView: InputView, resultView: ResultView) { - while (player.decideAction()) { + while (player.canHit() == BlackJackAction.HIT) { if (!inputView.askForAdditionalCard(player.name)) { return } - player.drawCard(deck) + player.receiveCard(deck.drawCard()) resultView.showHandCards(player) } println() From a8893d02b40d2426b0dd68069cfc7fbdf91531d9 Mon Sep 17 00:00:00 2001 From: bmsk Date: Fri, 24 Nov 2023 16:39:58 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[Feat]=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=93=A4=EC=9D=84=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/blackjack/dealer/Dealer.kt | 7 ++++--- .../blackjack/dealer/DefaultDealerStrategy.kt | 21 +++++++++++-------- domain/src/main/kotlin/blackjack/hand/Hand.kt | 7 +++++++ .../kotlin/blackjack/hand/StandardHand.kt | 11 ++++------ .../main/kotlin/blackjack/player/Player.kt | 4 ++-- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt index 22dc0eff7c..2a2013b54b 100644 --- a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt +++ b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt @@ -5,16 +5,17 @@ import blackjack.BlackjackParticipant import blackjack.card.Card import blackjack.deck.Deck import blackjack.hand.Hand +import blackjack.hand.StandardHand -data class Dealer( - override val hand: Hand, +class Dealer( + override val hand: Hand = StandardHand(), private val dealerStrategy: DealerStrategy = DefaultDealerStrategy() ) : BlackjackParticipant { val cards: List get() = hand.cards.toList() - override fun receiveCard(card: Card): Dealer = copy(hand = hand.addCard(card)) + override fun receiveCard(card: Card): Dealer = Dealer(hand.addCard(card)) override fun calculateBestValue(): Int = hand.calculateBestValue() diff --git a/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt index 9bb80351ce..c4668e2aff 100644 --- a/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt +++ b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt @@ -1,6 +1,7 @@ package blackjack.dealer import action.BlackJackAction +import blackjack.card.Card import blackjack.card.CardRank import blackjack.deck.Deck import blackjack.hand.Hand @@ -19,16 +20,18 @@ internal class DefaultDealerStrategy : DealerStrategy { } private fun calculateProbabilityOfBusting(currentScore: Int, deck: Deck): Double { - val scoreNeededToAvoidBust = 21 - currentScore - val safeCards = deck.remainingCards.count { card -> - val cardValue = when (card.rank) { - CardRank.KING, CardRank.QUEEN, CardRank.JACK -> 10 - CardRank.ACE -> 11 - else -> card.rank.ordinal + 1 - } - cardValue <= scoreNeededToAvoidBust - } + val remainedScore = 21 - currentScore + val safeCards = deck.remainingCards.count { isSafe(it, remainedScore) } return 1.0 - safeCards.toDouble() / deck.size.toDouble() } + + private fun isSafe(card: Card, remainedScore: Int): Boolean { + val cardValue = when (card.rank) { + CardRank.KING, CardRank.QUEEN, CardRank.JACK -> 10 + CardRank.ACE -> 11 + else -> card.rank.ordinal + 1 + } + return cardValue <= remainedScore + } } diff --git a/domain/src/main/kotlin/blackjack/hand/Hand.kt b/domain/src/main/kotlin/blackjack/hand/Hand.kt index 285f4f0574..37b048ac6e 100644 --- a/domain/src/main/kotlin/blackjack/hand/Hand.kt +++ b/domain/src/main/kotlin/blackjack/hand/Hand.kt @@ -7,4 +7,11 @@ interface Hand { fun addCard(card: Card): Hand fun calculateMinValue(): Int fun calculateBestValue(): Int + + companion object { + const val FACE_CARD_VALUE = 10 + const val MAX_HAND_VALUE = 21 + const val ACE_HIGH_VALUE = 11 + const val ACE_LOW_VALUE = 1 + } } diff --git a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt index 5b73f0b867..2bac142d83 100644 --- a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt +++ b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt @@ -2,6 +2,10 @@ package blackjack.hand import blackjack.card.Card import blackjack.card.CardRank +import blackjack.hand.Hand.Companion.ACE_HIGH_VALUE +import blackjack.hand.Hand.Companion.ACE_LOW_VALUE +import blackjack.hand.Hand.Companion.FACE_CARD_VALUE +import blackjack.hand.Hand.Companion.MAX_HAND_VALUE internal class StandardHand( override val cards: Set = emptySet() @@ -40,11 +44,4 @@ internal class StandardHand( } return sum } - - companion object { - private const val FACE_CARD_VALUE = 10 - private const val MAX_HAND_VALUE = 21 - private const val ACE_HIGH_VALUE = 11 - private const val ACE_LOW_VALUE = 1 - } } diff --git a/domain/src/main/kotlin/blackjack/player/Player.kt b/domain/src/main/kotlin/blackjack/player/Player.kt index 1575c37a33..24f0f2d784 100644 --- a/domain/src/main/kotlin/blackjack/player/Player.kt +++ b/domain/src/main/kotlin/blackjack/player/Player.kt @@ -6,7 +6,7 @@ import blackjack.card.Card import blackjack.hand.Hand import blackjack.hand.StandardHand -data class Player( +class Player( val name: String, override val hand: Hand = StandardHand(), ) : BlackjackParticipant { @@ -21,7 +21,7 @@ data class Player( } override fun receiveCard(card: Card): Player { - return copy(hand = hand.addCard(card)) + return Player(name, hand = hand.addCard(card)) } override fun calculateBestValue(): Int = hand.calculateBestValue() From 8ef13bf3107ad5152fc88ca0fc57ff1ff25dab11 Mon Sep 17 00:00:00 2001 From: bmsk Date: Fri, 24 Nov 2023 16:49:57 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[Docs]=20=EC=82=AC=EC=A0=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B3=84=ED=9A=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BLACKJACK.md | 34 ++++++++++++++++++- .../src/main/kotlin/ui/input/InputView.kt | 11 ------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/BLACKJACK.md b/docs/BLACKJACK.md index 0b5206a5f4..9ac7b733e0 100644 --- a/docs/BLACKJACK.md +++ b/docs/BLACKJACK.md @@ -61,4 +61,36 @@ jason카드: 7클로버, K스페이드 - 결과: 17 - [ ] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. - [x] 만일 딜러가 을 갖고 있다면? - - [x] 딜러는 다양한 전략을 사용할 수 있도록 한다. \ No newline at end of file + - [x] 딜러는 다양한 전략을 사용할 수 있도록 한다. + +### 도메인 모델들이 결정되었다면 블랙잭 게임 구현 계획 +도메인 모듈이 뷰에게 의존하지 않도록 구현할 것이다. + +- [ ] **게임 초기화 상태 확인** + - [ ] blackjackGame이 게임을 초기화해야 하는 상태인지 확인 + - [ ] 필요한 경우, blackjackGame에 필요한 정보를 사용자로부터 받아 전달 + +- [ ] **플레이어의 턴 상태 확인 (첫 번째)** + - [ ] blackjackGame이 플레이어들의 턴인 상태인지 확인 + - [ ] 현재 blackjackGame의 플레이어 중 누구의 턴인지 확인 + - [ ] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 + +- [ ] **플레이어의 턴 상태 재확인 (두 번째)** + - [ ] 다시 blackjackGame이 플레이어들의 턴인 상태인지 확인 + - [ ] 다시 현재 플레이어 중 누구의 턴인지 확인 + - [ ] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 + +- [ ] **마지막 플레이어의 턴과 딜러의 턴 상태 전환** + - [ ] 마지막 플레이어의 턴에서 더 이상 카드를 뽑을 수 없거나, 뽑지 않겠다는 결정이 내려진 경우 확인 + - [ ] blackjackGame의 상태를 딜러의 턴 상태로 전환 + +- [ ] **딜러의 턴 상태 확인** + - [ ] blackjackGame이 딜러의 턴인 상태인지 확인 + - [ ] 딜러가 카드를 뽑을지 안 뽑을지 결정하고 사용자에게 알림 + - [ ] blackjackGame을 `ResultPage` 상태로 전환 + +- [ ] **ResultPage 상태 확인** + - [ ] blackjackGame이 ResultPage 상태인지 확인 + - [ ] blackjackGame에게 ResultReport를 요청하고 사용자에게 보여줌 + +- [ ] **게임 종료** diff --git a/presenter/src/main/kotlin/ui/input/InputView.kt b/presenter/src/main/kotlin/ui/input/InputView.kt index 72902bcc40..039459a3f1 100644 --- a/presenter/src/main/kotlin/ui/input/InputView.kt +++ b/presenter/src/main/kotlin/ui/input/InputView.kt @@ -2,15 +2,4 @@ package ui.input class InputView { - fun inputPlayerNames(): List { - println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") - val input = readln() - return input.split(",").map { it.trim() } - } - - fun askForAdditionalCard(playerName: String): Boolean { - println("${playerName}은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") - val input = readln() - return input.equals("y", ignoreCase = true) - } } From c28a626159c4321401b1b2f10dafb02ebf4d0186 Mon Sep 17 00:00:00 2001 From: bmsk Date: Sat, 25 Nov 2023 12:30:38 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[Feat]=20BlackjackGame=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=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 - 생성 테스트 확인 --- .../kotlin/blackjack/BlackjackParticipant.kt | 2 - .../main/kotlin/blackjack/dealer/Dealer.kt | 9 ++- .../kotlin/blackjack/game/BlackjackGame.kt | 57 +++++++++++++++++++ domain/src/main/kotlin/blackjack/hand/Hand.kt | 2 +- .../kotlin/blackjack/hand/StandardHand.kt | 4 +- .../main/kotlin/blackjack/player/Player.kt | 9 ++- .../blackjack/game/BlackjackGameTest.kt | 43 ++++++++++++++ .../src/main/kotlin/ui/input/InputView.kt | 10 ++++ 8 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 domain/src/main/kotlin/blackjack/game/BlackjackGame.kt create mode 100644 domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt diff --git a/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt index 5096cf05b5..5f4113f9cb 100644 --- a/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt +++ b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt @@ -1,10 +1,8 @@ package blackjack import blackjack.card.Card -import blackjack.hand.Hand interface BlackjackParticipant { - val hand: Hand fun receiveCard(card: Card): BlackjackParticipant fun calculateBestValue(): Int } diff --git a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt index 2a2013b54b..96d4b63b2e 100644 --- a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt +++ b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt @@ -7,13 +7,12 @@ import blackjack.deck.Deck import blackjack.hand.Hand import blackjack.hand.StandardHand -class Dealer( - override val hand: Hand = StandardHand(), - private val dealerStrategy: DealerStrategy = DefaultDealerStrategy() +data class Dealer( + val hand: Hand = StandardHand(), + val dealerStrategy: DealerStrategy = DefaultDealerStrategy() ) : BlackjackParticipant { - val cards: List - get() = hand.cards.toList() + val cards: List = hand.cards() override fun receiveCard(card: Card): Dealer = Dealer(hand.addCard(card)) diff --git a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt new file mode 100644 index 0000000000..67a3a2ddb3 --- /dev/null +++ b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt @@ -0,0 +1,57 @@ +package blackjack.game + +import blackjack.dealer.Dealer +import blackjack.dealer.DealerStrategy +import blackjack.dealer.DefaultDealerStrategy +import blackjack.player.Player + +class BlackjackGame private constructor( + val players: List, + val dealer: Dealer = Dealer() +) { + init { + require(players.toSet().isNotEmpty()) { "플레이어가 최소 한 명은 존재해야 합니다." } + } + + class BlackjackGameBuilder { + private val players: MutableList = mutableListOf() + private var dealerStrategy: DealerStrategy = DefaultDealerStrategy() + + fun join(name: String) { + players.add(Player(name = name)) + } + + fun join(vararg names: String) { + names.forEach { + join(it) + } + } + + fun join(names: List) { + names.forEach { + join(it) + } + } + + fun dealerStrategy(strategy: DealerStrategyType) { + when (strategy) { + DealerStrategyType.DEFAULT_DEALER_STRATEGY -> dealerStrategy = DefaultDealerStrategy() + // 다른 전략 추가 + } + } + + fun build(): BlackjackGame { + return BlackjackGame( + players = players.toList(), + dealer = Dealer(dealerStrategy = dealerStrategy) + ) + } + } +} + +enum class DealerStrategyType { + DEFAULT_DEALER_STRATEGY +} + +fun blackjackOpen(block: BlackjackGame.BlackjackGameBuilder.() -> Unit): BlackjackGame = + BlackjackGame.BlackjackGameBuilder().apply(block).build() diff --git a/domain/src/main/kotlin/blackjack/hand/Hand.kt b/domain/src/main/kotlin/blackjack/hand/Hand.kt index 37b048ac6e..0c70396fae 100644 --- a/domain/src/main/kotlin/blackjack/hand/Hand.kt +++ b/domain/src/main/kotlin/blackjack/hand/Hand.kt @@ -3,7 +3,7 @@ package blackjack.hand import blackjack.card.Card interface Hand { - val cards: Set + fun cards(): List fun addCard(card: Card): Hand fun calculateMinValue(): Int fun calculateBestValue(): Int diff --git a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt index 2bac142d83..4c7efff0dc 100644 --- a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt +++ b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt @@ -8,8 +8,10 @@ import blackjack.hand.Hand.Companion.FACE_CARD_VALUE import blackjack.hand.Hand.Companion.MAX_HAND_VALUE internal class StandardHand( - override val cards: Set = emptySet() + val cards: Set = emptySet() ) : Hand { + override fun cards(): List = cards.toList() + override fun addCard(card: Card): StandardHand = StandardHand(cards = cards + card) override fun calculateBestValue(): Int { diff --git a/domain/src/main/kotlin/blackjack/player/Player.kt b/domain/src/main/kotlin/blackjack/player/Player.kt index 24f0f2d784..2f2d27efa0 100644 --- a/domain/src/main/kotlin/blackjack/player/Player.kt +++ b/domain/src/main/kotlin/blackjack/player/Player.kt @@ -6,13 +6,12 @@ import blackjack.card.Card import blackjack.hand.Hand import blackjack.hand.StandardHand -class Player( +data class Player( val name: String, - override val hand: Hand = StandardHand(), + val hand: Hand = StandardHand() ) : BlackjackParticipant { - val cards: List - get() = hand.cards.toList() + val cards: List = hand.cards() fun canHit(): BlackJackAction = if (hand.calculateMinValue() <= 21) { BlackJackAction.HIT @@ -21,7 +20,7 @@ class Player( } override fun receiveCard(card: Card): Player { - return Player(name, hand = hand.addCard(card)) + return copy(hand = hand.addCard(card)) } override fun calculateBestValue(): Int = hand.calculateBestValue() diff --git a/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt new file mode 100644 index 0000000000..31d37df172 --- /dev/null +++ b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt @@ -0,0 +1,43 @@ +package blackjack.game + +import blackjack.dealer.DefaultDealerStrategy +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.matchers.types.shouldBeInstanceOf + +class BlackjackGameTest : StringSpec({ + + "플레이어 추가 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + } + game.players.map { it.name } shouldContainExactly listOf("Alice", "Bob") + } + + "딜러 전략 설정 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + dealerStrategy(DealerStrategyType.DEFAULT_DEALER_STRATEGY) + } + game.dealer.dealerStrategy.shouldBeInstanceOf() + } + + "게임 인스턴스 생성 확인" { + val game = blackjackOpen { + join("Charlie") + } + game shouldNotBe null + } + + "플레이어 없이 게임 생성 시 예외 발생 확인" { + val exception = shouldThrow { + blackjackOpen { } + } + exception shouldHaveMessage "플레이어가 최소 한 명은 존재해야 합니다." + } +}) diff --git a/presenter/src/main/kotlin/ui/input/InputView.kt b/presenter/src/main/kotlin/ui/input/InputView.kt index 039459a3f1..88fcc74658 100644 --- a/presenter/src/main/kotlin/ui/input/InputView.kt +++ b/presenter/src/main/kotlin/ui/input/InputView.kt @@ -1,5 +1,15 @@ package ui.input class InputView { + fun inputPlayerNames(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + val input = readln() + return input.split(",").map { it.trim() } + } + fun askForAdditionalCard(playerName: String): Boolean { + println("${playerName}은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + val input = readln() + return input.equals("y", ignoreCase = true) + } } From 8ec89194d5d7c005d1f20a12270cf0819a92a352 Mon Sep 17 00:00:00 2001 From: bmsk Date: Sat, 25 Nov 2023 13:04:24 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[Feat]=20=EC=B4=88=EA=B8=B0=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B0=B0=EB=B6=84=20=EB=A1=9C=EC=A7=81=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 --- docs/BLACKJACK.md | 6 +++--- .../main/kotlin/blackjack/BlackjackParticipant.kt | 1 + domain/src/main/kotlin/blackjack/dealer/Dealer.kt | 10 ++++++---- .../blackjack/dealer/DefaultDealerStrategy.kt | 2 +- domain/src/main/kotlin/blackjack/deck/Deck.kt | 10 ++++++---- .../main/kotlin/blackjack/game/BlackjackGame.kt | 14 +++++++++++++- domain/src/main/kotlin/blackjack/hand/Hand.kt | 1 + .../src/main/kotlin/blackjack/hand/StandardHand.kt | 2 ++ domain/src/main/kotlin/blackjack/player/Player.kt | 10 +++++----- domain/src/test/kotlin/blackjack/deck/DeckTest.kt | 4 ++-- .../kotlin/blackjack/game/BlackjackGameTest.kt | 14 ++++++++++++++ presenter/src/main/kotlin/ui/input/InputView.kt | 1 + 12 files changed, 55 insertions(+), 20 deletions(-) diff --git a/docs/BLACKJACK.md b/docs/BLACKJACK.md index 9ac7b733e0..c242848259 100644 --- a/docs/BLACKJACK.md +++ b/docs/BLACKJACK.md @@ -66,9 +66,9 @@ jason카드: 7클로버, K스페이드 - 결과: 17 ### 도메인 모델들이 결정되었다면 블랙잭 게임 구현 계획 도메인 모듈이 뷰에게 의존하지 않도록 구현할 것이다. -- [ ] **게임 초기화 상태 확인** - - [ ] blackjackGame이 게임을 초기화해야 하는 상태인지 확인 - - [ ] 필요한 경우, blackjackGame에 필요한 정보를 사용자로부터 받아 전달 +- [x] **게임 초기화 상태 확인** + - [x] blackjackGame이 게임을 초기화해야 하는 상태인지 확인 + - [x] 필요한 경우, blackjackGame에 필요한 정보를 사용자로부터 받아 전달 - [ ] **플레이어의 턴 상태 확인 (첫 번째)** - [ ] blackjackGame이 플레이어들의 턴인 상태인지 확인 diff --git a/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt index 5f4113f9cb..a5e46cb21c 100644 --- a/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt +++ b/domain/src/main/kotlin/blackjack/BlackjackParticipant.kt @@ -4,5 +4,6 @@ import blackjack.card.Card interface BlackjackParticipant { fun receiveCard(card: Card): BlackjackParticipant + fun receiveCard(cards: List): BlackjackParticipant fun calculateBestValue(): Int } diff --git a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt index 96d4b63b2e..c7938dd491 100644 --- a/domain/src/main/kotlin/blackjack/dealer/Dealer.kt +++ b/domain/src/main/kotlin/blackjack/dealer/Dealer.kt @@ -8,13 +8,15 @@ import blackjack.hand.Hand import blackjack.hand.StandardHand data class Dealer( - val hand: Hand = StandardHand(), - val dealerStrategy: DealerStrategy = DefaultDealerStrategy() + val dealerStrategy: DealerStrategy = DefaultDealerStrategy(), + private val hand: Hand = StandardHand(), ) : BlackjackParticipant { - val cards: List = hand.cards() + val cards: List get() = hand.cards() - override fun receiveCard(card: Card): Dealer = Dealer(hand.addCard(card)) + override fun receiveCard(card: Card): Dealer = copy(hand = hand.addCard(card)) + + override fun receiveCard(cards: List): Dealer = copy(hand = hand.addCard(cards)) override fun calculateBestValue(): Int = hand.calculateBestValue() diff --git a/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt index c4668e2aff..1d0815f04b 100644 --- a/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt +++ b/domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt @@ -23,7 +23,7 @@ internal class DefaultDealerStrategy : DealerStrategy { val remainedScore = 21 - currentScore val safeCards = deck.remainingCards.count { isSafe(it, remainedScore) } - return 1.0 - safeCards.toDouble() / deck.size.toDouble() + return 1.0 - safeCards.toDouble() / deck.remainingCards.size } private fun isSafe(card: Card, remainedScore: Int): Boolean { diff --git a/domain/src/main/kotlin/blackjack/deck/Deck.kt b/domain/src/main/kotlin/blackjack/deck/Deck.kt index f9cc2bd4a9..ccb068e60d 100644 --- a/domain/src/main/kotlin/blackjack/deck/Deck.kt +++ b/domain/src/main/kotlin/blackjack/deck/Deck.kt @@ -1,7 +1,7 @@ package blackjack.deck import blackjack.card.Card -import java.util.Stack +import java.util.* class Deck( cardProvider: CardProvider = StandardCardProvider(), @@ -14,11 +14,13 @@ class Deck( val remainingCards: List get() = cards.toList() - val size - get() = cards.size - fun drawCard(): Card { check(cards.isNotEmpty()) { "덱에 카드가 없으면 카드를 뽑을 수 없습니다." } return cards.pop() } + + fun drawCard(count: Int): List { + check(cards.size >= count) { "덱에 $count 만큼 카드가 없습니다." } + return List(count) { cards.pop() } + } } diff --git a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt index 67a3a2ddb3..cebc57b2b9 100644 --- a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt +++ b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt @@ -3,16 +3,28 @@ package blackjack.game import blackjack.dealer.Dealer import blackjack.dealer.DealerStrategy import blackjack.dealer.DefaultDealerStrategy +import blackjack.deck.Deck import blackjack.player.Player class BlackjackGame private constructor( val players: List, - val dealer: Dealer = Dealer() + val dealer: Dealer = Dealer(), + private val deck: Deck = Deck(), ) { init { require(players.toSet().isNotEmpty()) { "플레이어가 최소 한 명은 존재해야 합니다." } } + fun dealInitialCards(): BlackjackGame { + val nPlayers = List(players.size) { players[it].receiveCard(deck.drawCard(2)) } + val nDealer = dealer.receiveCard(deck.drawCard(2)) + return BlackjackGame( + players = nPlayers, + dealer = nDealer, + deck = deck + ) + } + class BlackjackGameBuilder { private val players: MutableList = mutableListOf() private var dealerStrategy: DealerStrategy = DefaultDealerStrategy() diff --git a/domain/src/main/kotlin/blackjack/hand/Hand.kt b/domain/src/main/kotlin/blackjack/hand/Hand.kt index 0c70396fae..b527880bb0 100644 --- a/domain/src/main/kotlin/blackjack/hand/Hand.kt +++ b/domain/src/main/kotlin/blackjack/hand/Hand.kt @@ -5,6 +5,7 @@ import blackjack.card.Card interface Hand { fun cards(): List fun addCard(card: Card): Hand + fun addCard(cards: List): Hand fun calculateMinValue(): Int fun calculateBestValue(): Int diff --git a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt index 4c7efff0dc..3c245bef5c 100644 --- a/domain/src/main/kotlin/blackjack/hand/StandardHand.kt +++ b/domain/src/main/kotlin/blackjack/hand/StandardHand.kt @@ -14,6 +14,8 @@ internal class StandardHand( override fun addCard(card: Card): StandardHand = StandardHand(cards = cards + card) + override fun addCard(cards: List): StandardHand = StandardHand(cards = this.cards + cards) + override fun calculateBestValue(): Int { val sumWithoutAces = cards.filter { it.rank != CardRank.ACE }.sumOf { cardValue(it) } val aceCount = cards.count { it.rank == CardRank.ACE } diff --git a/domain/src/main/kotlin/blackjack/player/Player.kt b/domain/src/main/kotlin/blackjack/player/Player.kt index 2f2d27efa0..99d2b42355 100644 --- a/domain/src/main/kotlin/blackjack/player/Player.kt +++ b/domain/src/main/kotlin/blackjack/player/Player.kt @@ -8,10 +8,10 @@ import blackjack.hand.StandardHand data class Player( val name: String, - val hand: Hand = StandardHand() + private val hand: Hand = StandardHand() ) : BlackjackParticipant { - val cards: List = hand.cards() + val cards: List get() = hand.cards() fun canHit(): BlackJackAction = if (hand.calculateMinValue() <= 21) { BlackJackAction.HIT @@ -19,9 +19,9 @@ data class Player( BlackJackAction.STAND } - override fun receiveCard(card: Card): Player { - return copy(hand = hand.addCard(card)) - } + override fun receiveCard(card: Card): Player = copy(hand = hand.addCard(card)) + + override fun receiveCard(cards: List): Player = copy(hand = hand.addCard(cards)) override fun calculateBestValue(): Int = hand.calculateBestValue() } diff --git a/domain/src/test/kotlin/blackjack/deck/DeckTest.kt b/domain/src/test/kotlin/blackjack/deck/DeckTest.kt index aa798f3417..eb021ec23b 100644 --- a/domain/src/test/kotlin/blackjack/deck/DeckTest.kt +++ b/domain/src/test/kotlin/blackjack/deck/DeckTest.kt @@ -17,9 +17,9 @@ class DeckTest : StringSpec({ "drawCard 함수가 실행되면 덱의 사이즈가 1 줄어든다." { val deck = Deck(StandardCardProvider(), RandomCardShuffler()) - val initialSize = deck.size + val initialSize = deck.remainingCards deck.drawCard() - deck.size shouldBe (initialSize - 1) + deck.remainingCards shouldBe (initialSize - 1) } "덱이 비어있을 때 카드를 뽑으려고 하면 예외가 발생해야 한다" { diff --git a/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt index 31d37df172..7b680814ba 100644 --- a/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt +++ b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt @@ -4,6 +4,7 @@ import blackjack.dealer.DefaultDealerStrategy import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldNotBe import io.kotest.matchers.throwable.shouldHaveMessage import io.kotest.matchers.types.shouldBeInstanceOf @@ -40,4 +41,17 @@ class BlackjackGameTest : StringSpec({ } exception shouldHaveMessage "플레이어가 최소 한 명은 존재해야 합니다." } + + "초기 카드 배분 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + } + val newGame = game.dealInitialCards() + + newGame.players.forEach { player -> + player.cards shouldHaveSize 2 + } + newGame.dealer.cards shouldHaveSize 2 + } }) diff --git a/presenter/src/main/kotlin/ui/input/InputView.kt b/presenter/src/main/kotlin/ui/input/InputView.kt index 88fcc74658..4190164318 100644 --- a/presenter/src/main/kotlin/ui/input/InputView.kt +++ b/presenter/src/main/kotlin/ui/input/InputView.kt @@ -4,6 +4,7 @@ class InputView { fun inputPlayerNames(): List { println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") val input = readln() + input.ifBlank { throw IllegalArgumentException("한 명이라도 입력해야 합니다.") } return input.split(",").map { it.trim() } } From 976609fe58df009e7a771576d6e125ffecb7c5f6 Mon Sep 17 00:00:00 2001 From: bmsk Date: Sat, 25 Nov 2023 17:36:26 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[Feat]=20BlackjackGame=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=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 --- .../kotlin/blackjack/game/BlackjackGame.kt | 113 +++++++++++++++--- .../kotlin/blackjack/game/BlackjackResult.kt | 6 + .../main/kotlin/blackjack/game/GameState.kt | 33 +++++ .../test/kotlin/blackjack/deck/DeckTest.kt | 4 +- .../blackjack/game/BlackjackGameTest.kt | 53 +++++++- .../kotlin/model/BlackjackParticipants.kt | 9 ++ presenter/src/main/kotlin/ui/Main.kt | 69 +++++++---- .../src/main/kotlin/ui/input/InputView.kt | 1 + .../src/main/kotlin/ui/result/ResultView.kt | 54 +++++++-- 9 files changed, 289 insertions(+), 53 deletions(-) create mode 100644 domain/src/main/kotlin/blackjack/game/BlackjackResult.kt create mode 100644 domain/src/main/kotlin/blackjack/game/GameState.kt create mode 100644 presenter/src/main/kotlin/model/BlackjackParticipants.kt diff --git a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt index cebc57b2b9..be54aa1b0a 100644 --- a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt +++ b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt @@ -1,5 +1,8 @@ package blackjack.game +import action.BlackJackAction +import blackjack.BlackjackParticipant +import blackjack.card.Card import blackjack.dealer.Dealer import blackjack.dealer.DealerStrategy import blackjack.dealer.DefaultDealerStrategy @@ -7,22 +10,110 @@ import blackjack.deck.Deck import blackjack.player.Player class BlackjackGame private constructor( - val players: List, - val dealer: Dealer = Dealer(), + players: List, + dealer: Dealer = Dealer(), private val deck: Deck = Deck(), ) { init { require(players.toSet().isNotEmpty()) { "플레이어가 최소 한 명은 존재해야 합니다." } } - fun dealInitialCards(): BlackjackGame { + var state: GameState = GameState.InitialDeal(players, dealer) + private set + + val players: List get() = state.players + val dealer: Dealer get() = state.dealer + + fun dealInitialCards() { + check(state is GameState.InitialDeal) { "Initial Deal 상태가 아닙니다." } val nPlayers = List(players.size) { players[it].receiveCard(deck.drawCard(2)) } val nDealer = dealer.receiveCard(deck.drawCard(2)) - return BlackjackGame( - players = nPlayers, - dealer = nDealer, - deck = deck - ) + state = GameState.PlayerTurn(nPlayers, nDealer, currentPlayerIndex = 0) + } + + fun dealPlayerTurn(player: Player, isDeal: Boolean) { + val playerTurnState = state as? GameState.PlayerTurn ?: throw IllegalStateException("Player Turn이 아닙니다.") + require(players.contains(player)) { "${player.name}이라는 플레이어는 없습니다." } + require(player == playerTurnState.currentPlayer) { "현재 턴은 ${player.name}의 턴이 아닙니다." } + + if (isDeal.not()) { + // 다음 플레이어로 넘어감 + moveToNextPlayerOrDealerTurn(playerTurnState.currentPlayerIndex) + } else { + // 카드 받기 + check(player.canHit() == BlackJackAction.HIT) { "해당 플레이어는 더 이상 카드를 받을 수 없습니다." } + val nPlayers = players.map { if (it == player) it.receiveCard(deck.drawCard()) else it } + state = GameState.PlayerTurn(nPlayers, dealer, playerTurnState.currentPlayerIndex) + } + } + + fun dealDealerTurn(): BlackJackAction { + check(state is GameState.DealerTurn) { "Dealer Turn이 아닙니다." } + return when (dealer.decideAction(deck)) { + BlackJackAction.HIT -> { + state = GameState.End( + players, + dealer.receiveCard(deck.drawCard()) + ) + BlackJackAction.HIT + } + + BlackJackAction.STAND -> { + state = GameState.End(players, dealer) + BlackJackAction.STAND + } + } + } + + fun calculateResult(): Map { + val results = mutableMapOf() + results[dealer] = calculateDealerResult() + players.forEach { + results[it] = calculatePlayerResult(it) + } + return results + } + + fun showPlayerCards(playerName: String): List { + val player = state.players.find { it.name == playerName } + ?: throw IllegalArgumentException("${playerName}이라는 플레이어는 없습니다.") + return player.cards + } + + private fun calculateDealerResult(): BlackjackResult { + val dealerScore = dealer.calculateBestValue() + var win = 0 + var loss = 0 + players.forEach { + if (dealerScore > 21) loss++ + else if (it.calculateBestValue() > 21) win++ + else if (dealerScore > it.calculateBestValue()) win++ + else if (dealerScore <= it.calculateBestValue()) loss++ + } + return BlackjackResult(win, loss) + } + + private fun calculatePlayerResult(player: Player): BlackjackResult { + val playerScore = player.calculateBestValue() + val dealerScore = dealer.calculateBestValue() + return if (dealerScore > 21) { + BlackjackResult(1, 0) + } else if (playerScore > 21) { + BlackjackResult(0, 1) + } else if (playerScore >= dealerScore) { + BlackjackResult(1, 0) + } else { + BlackjackResult(0, 1) + } + } + + private fun moveToNextPlayerOrDealerTurn(currentPlayerIndex: Int) { + val nextPlayerIndex = (currentPlayerIndex + 1) % players.size + state = if (nextPlayerIndex == 0) { + GameState.DealerTurn(players, dealer) + } else { + GameState.PlayerTurn(players, dealer, nextPlayerIndex) + } } class BlackjackGameBuilder { @@ -33,12 +124,6 @@ class BlackjackGame private constructor( players.add(Player(name = name)) } - fun join(vararg names: String) { - names.forEach { - join(it) - } - } - fun join(names: List) { names.forEach { join(it) diff --git a/domain/src/main/kotlin/blackjack/game/BlackjackResult.kt b/domain/src/main/kotlin/blackjack/game/BlackjackResult.kt new file mode 100644 index 0000000000..9056c754e6 --- /dev/null +++ b/domain/src/main/kotlin/blackjack/game/BlackjackResult.kt @@ -0,0 +1,6 @@ +package blackjack.game + +data class BlackjackResult( + val win: Int, + val lose: Int, +) diff --git a/domain/src/main/kotlin/blackjack/game/GameState.kt b/domain/src/main/kotlin/blackjack/game/GameState.kt new file mode 100644 index 0000000000..aca64a967d --- /dev/null +++ b/domain/src/main/kotlin/blackjack/game/GameState.kt @@ -0,0 +1,33 @@ +package blackjack.game + +import blackjack.dealer.Dealer +import blackjack.player.Player + +sealed class GameState( + val players: List, + val dealer: Dealer, +) { + class InitialDeal( + players: List, + dealer: Dealer, + ) : GameState(players, dealer) + + class PlayerTurn( + players: List, + dealer: Dealer, + val currentPlayerIndex: Int, + ) : GameState(players, dealer) { + val currentPlayer: Player + get() = players[currentPlayerIndex] + } + + class DealerTurn( + players: List, + dealer: Dealer, + ) : GameState(players, dealer) + + class End( + players: List, + dealer: Dealer, + ) : GameState(players, dealer) +} diff --git a/domain/src/test/kotlin/blackjack/deck/DeckTest.kt b/domain/src/test/kotlin/blackjack/deck/DeckTest.kt index eb021ec23b..a13fe5892a 100644 --- a/domain/src/test/kotlin/blackjack/deck/DeckTest.kt +++ b/domain/src/test/kotlin/blackjack/deck/DeckTest.kt @@ -17,9 +17,9 @@ class DeckTest : StringSpec({ "drawCard 함수가 실행되면 덱의 사이즈가 1 줄어든다." { val deck = Deck(StandardCardProvider(), RandomCardShuffler()) - val initialSize = deck.remainingCards + val initialSize = deck.remainingCards.size deck.drawCard() - deck.remainingCards shouldBe (initialSize - 1) + deck.remainingCards.size shouldBe (initialSize - 1) } "덱이 비어있을 때 카드를 뽑으려고 하면 예외가 발생해야 한다" { diff --git a/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt index 7b680814ba..fcf84a3c4b 100644 --- a/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt +++ b/domain/src/test/kotlin/blackjack/game/BlackjackGameTest.kt @@ -1,10 +1,13 @@ package blackjack.game +import action.BlackJackAction import blackjack.dealer.DefaultDealerStrategy import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.throwable.shouldHaveMessage import io.kotest.matchers.types.shouldBeInstanceOf @@ -47,11 +50,55 @@ class BlackjackGameTest : StringSpec({ join("Alice") join("Bob") } - val newGame = game.dealInitialCards() + game.dealInitialCards() - newGame.players.forEach { player -> + game.players.forEach { player -> player.cards shouldHaveSize 2 } - newGame.dealer.cards shouldHaveSize 2 + game.dealer.cards shouldHaveSize 2 + } + + "플레이어 턴 처리 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + } + game.dealInitialCards() + val alice = game.players.first { it.name == "Alice" } + + // Alice가 턴을 끝냄 + game.dealPlayerTurn(alice, false) + alice.cards shouldHaveSize 2 + + // Bob의 턴으로 넘어감 + val bob = game.players.first { it.name == "Bob" } + (game.state as GameState.PlayerTurn).currentPlayer shouldBe bob + } + + "딜러 턴 처리 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + } + game.dealInitialCards() + // 모든 플레이어의 턴을 종료 + game.players.forEach { game.dealPlayerTurn(it, false) } + + // 딜러 턴 시작 + game.state.shouldBeInstanceOf() + } + + "게임 결과 계산 확인" { + val game = blackjackOpen { + join("Alice") + join("Bob") + } + game.dealInitialCards() + // 모든 턴 종료 + game.players.forEach { game.dealPlayerTurn(it, false) } + game.dealDealerTurn() + + val results = game.calculateResult() + results.keys shouldContainExactlyInAnyOrder game.players + game.dealer } }) diff --git a/presenter/src/main/kotlin/model/BlackjackParticipants.kt b/presenter/src/main/kotlin/model/BlackjackParticipants.kt new file mode 100644 index 0000000000..e13d2385ef --- /dev/null +++ b/presenter/src/main/kotlin/model/BlackjackParticipants.kt @@ -0,0 +1,9 @@ +package model + +import blackjack.dealer.Dealer +import blackjack.player.Player + +data class BlackjackParticipants( + val dealer: Dealer, + val players: List +) diff --git a/presenter/src/main/kotlin/ui/Main.kt b/presenter/src/main/kotlin/ui/Main.kt index fb25b71ce9..b3efab04d8 100644 --- a/presenter/src/main/kotlin/ui/Main.kt +++ b/presenter/src/main/kotlin/ui/Main.kt @@ -1,8 +1,9 @@ package ui -import action.BlackJackAction -import blackjack.deck.Deck -import blackjack.player.Player +import blackjack.game.BlackjackGame +import blackjack.game.DealerStrategyType +import blackjack.game.GameState +import blackjack.game.blackjackOpen import ui.input.InputView import ui.result.ResultView @@ -11,30 +12,56 @@ fun main() { val resultView = ResultView() val playerNames = inputView.inputPlayerNames() - val deck = Deck() - val players = playerNames.map { Player(it) } + val blackjackGame = blackjackOpen { + join(playerNames) + dealerStrategy(DealerStrategyType.DEFAULT_DEALER_STRATEGY) + } - playBlackjack(deck, players, inputView, resultView) + while (blackjackGame.state !is GameState.End) { + processGameState(blackjackGame, inputView, resultView) + } + processGameState(blackjackGame, inputView, resultView) } -fun playBlackjack(deck: Deck, players: List, inputView: InputView, resultView: ResultView) { - val playingPlayers = resultView.showInitialCards(deck, players).toMutableList() - println() - - playingPlayers.forEach { player -> - handlePlayerTurn(deck, player, inputView, resultView) +private fun processGameState(blackjackGame: BlackjackGame, inputView: InputView, resultView: ResultView) { + when (val gameState = blackjackGame.state) { + is GameState.PlayerTurn -> processPlayerTurn(gameState, blackjackGame, inputView, resultView) + is GameState.DealerTurn -> processDealerTurn(blackjackGame, resultView) + is GameState.InitialDeal -> processInitialDeal(blackjackGame, resultView) + is GameState.End -> processGameEnd(blackjackGame, resultView) } +} - resultView.showFinalResults(playingPlayers) +private fun processPlayerTurn( + gameState: GameState.PlayerTurn, + blackjackGame: BlackjackGame, + inputView: InputView, + resultView: ResultView +) { + val currentPlayer = gameState.currentPlayer + val isDeal = inputView.askForAdditionalCard(currentPlayer.name) + blackjackGame.dealPlayerTurn(currentPlayer, isDeal) + resultView.showCards(currentPlayer.name, blackjackGame.showPlayerCards(currentPlayer.name)) } -fun handlePlayerTurn(deck: Deck, player: Player, inputView: InputView, resultView: ResultView) { - while (player.canHit() == BlackJackAction.HIT) { - if (!inputView.askForAdditionalCard(player.name)) { - return - } - player.receiveCard(deck.drawCard()) - resultView.showHandCards(player) - } +private fun processDealerTurn(blackjackGame: BlackjackGame, resultView: ResultView) { + val action = blackjackGame.dealDealerTurn() + resultView.showDealerTurn(action) +} + +private fun processInitialDeal(blackjackGame: BlackjackGame, resultView: ResultView) { + blackjackGame.dealInitialCards() + resultView.showInitialDealMessage(blackjackGame.players) + resultView.showCards(blackjackGame.dealer) + resultView.showCards(blackjackGame.players) +} + +private fun processGameEnd(blackjackGame: BlackjackGame, resultView: ResultView) { println() + val result = blackjackGame.calculateResult() + + resultView.showCards(blackjackGame.dealer) + blackjackGame.players.forEach { resultView.showCards(it) } + + resultView.showParticipantsRecord(blackjackGame, result) } diff --git a/presenter/src/main/kotlin/ui/input/InputView.kt b/presenter/src/main/kotlin/ui/input/InputView.kt index 4190164318..bd136b9f66 100644 --- a/presenter/src/main/kotlin/ui/input/InputView.kt +++ b/presenter/src/main/kotlin/ui/input/InputView.kt @@ -4,6 +4,7 @@ class InputView { fun inputPlayerNames(): List { println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") val input = readln() + println() input.ifBlank { throw IllegalArgumentException("한 명이라도 입력해야 합니다.") } return input.split(",").map { it.trim() } } diff --git a/presenter/src/main/kotlin/ui/result/ResultView.kt b/presenter/src/main/kotlin/ui/result/ResultView.kt index 3eaf02cb91..09a87dd12a 100644 --- a/presenter/src/main/kotlin/ui/result/ResultView.kt +++ b/presenter/src/main/kotlin/ui/result/ResultView.kt @@ -1,31 +1,59 @@ package ui.result -import blackjack.deck.Deck +import action.BlackJackAction +import blackjack.BlackjackParticipant +import blackjack.card.Card +import blackjack.dealer.Dealer +import blackjack.game.BlackjackGame +import blackjack.game.BlackjackResult import blackjack.player.Player import toUiString class ResultView { - private fun showCardsForPlayer(player: Player) { - println("${player.name}카드: ${player.cards.joinToString(", ") { it.toUiString() }}") + fun showCards(cardOwner: String, cards: List) { + println("${cardOwner}카드: ${cards.joinToString(", ") { it.toUiString() }}") } - fun showInitialCards(deck: Deck, players: List): List { - val resultPlayers = players.take(2).map { it.receiveCard(deck.drawCard()).receiveCard(deck.drawCard()) } + fun showCards(participant: BlackjackParticipant) { + when (participant) { + is Dealer -> println("딜러 카드: ${participant.cards.joinToString(", ") { it.toUiString() }}") + is Player -> println("${participant.name} 카드: ${participant.cards.joinToString(", ") { it.toUiString() }}") + } + } - println("${resultPlayers.joinToString(", ") { it.name }}에게 2장의 나누었습니다.") - resultPlayers.forEach(this::showCardsForPlayer) + fun showCards(players: List) { + players.forEach(::showCards) + println() + } - return resultPlayers + fun showDealerTurn(dealerAction: BlackJackAction) { + val message = if (dealerAction == BlackJackAction.HIT) { + "딜러는 16 이하라 1장 더 받습니다." + } else { + "딜러는 16을 초과하여 받지 않습니다." + } + println(message) } - fun showHandCards(player: Player) { - showCardsForPlayer(player) + fun showInitialDealMessage(players: List) { + println("딜러와 ${players.joinToString(", "){ it.name }}에게 2장의 카드를 나누었습니다.") } - fun showFinalResults(players: List) { - players.forEach { player -> - println("${player.name}카드: ${player.cards.joinToString(", ") { it.toUiString() }} - 결과: ${player.calculateBestValue()}") + fun showParticipantsRecord(blackjackGame: BlackjackGame, result: Map) { + println() + println("## 최종 승패") + showDealerRecord(result[blackjackGame.dealer] ?: throw IllegalArgumentException()) + blackjackGame.players.forEach { + showPlayerRecord(it, result[it] ?: throw IllegalArgumentException()) } } + private fun showDealerRecord(dealerResult: BlackjackResult) { + println("딜러: ${dealerResult.win}승 ${dealerResult.lose}패") + } + + private fun showPlayerRecord(player: Player, playerResult: BlackjackResult) { + val winOrLose = if (playerResult.win == 1) "승" else "패" + println("${player.name}: $winOrLose") + } } From 8b2ff4c0f3fa7f7214c6a721ea410c2a69e6228b Mon Sep 17 00:00:00 2001 From: bmsk Date: Sat, 25 Nov 2023 17:49:51 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[Refact]=20=EC=9D=B8=EB=8D=B4=ED=8A=B8?= =?UTF-8?q?=EB=8A=94=20=ED=95=9C=20=EB=B2=88=EB=A7=8C=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BLACKJACK.md | 46 +++++++++---------- .../kotlin/blackjack/game/BlackjackGame.kt | 25 ++++------ .../src/main/kotlin/ui/result/ResultView.kt | 8 ++-- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/docs/BLACKJACK.md b/docs/BLACKJACK.md index c242848259..86db4d9faa 100644 --- a/docs/BLACKJACK.md +++ b/docs/BLACKJACK.md @@ -53,13 +53,13 @@ jason카드: 7클로버, K스페이드 - 결과: 17 ### 3. 게임 규칙 및 결과 판정 -- [ ] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다. -- [ ] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다. -- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다. +- [x] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다. +- [x] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다. +- [x] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다. ## 딜러가 추가된 사전 구현 계획 -- [ ] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. +- [x] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. - [x] 만일 딜러가 을 갖고 있다면? - [x] 딜러는 다양한 전략을 사용할 수 있도록 한다. @@ -70,27 +70,27 @@ jason카드: 7클로버, K스페이드 - 결과: 17 - [x] blackjackGame이 게임을 초기화해야 하는 상태인지 확인 - [x] 필요한 경우, blackjackGame에 필요한 정보를 사용자로부터 받아 전달 -- [ ] **플레이어의 턴 상태 확인 (첫 번째)** - - [ ] blackjackGame이 플레이어들의 턴인 상태인지 확인 - - [ ] 현재 blackjackGame의 플레이어 중 누구의 턴인지 확인 - - [ ] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 +- [x] **플레이어의 턴 상태 확인 (첫 번째)** + - [x] blackjackGame이 플레이어들의 턴인 상태인지 확인 + - [x] 현재 blackjackGame의 플레이어 중 누구의 턴인지 확인 + - [x] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 -- [ ] **플레이어의 턴 상태 재확인 (두 번째)** - - [ ] 다시 blackjackGame이 플레이어들의 턴인 상태인지 확인 - - [ ] 다시 현재 플레이어 중 누구의 턴인지 확인 - - [ ] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 +- [x] **플레이어의 턴 상태 재확인 (두 번째)** + - [x] 다시 blackjackGame이 플레이어들의 턴인 상태인지 확인 + - [x] 다시 현재 플레이어 중 누구의 턴인지 확인 + - [x] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달 -- [ ] **마지막 플레이어의 턴과 딜러의 턴 상태 전환** - - [ ] 마지막 플레이어의 턴에서 더 이상 카드를 뽑을 수 없거나, 뽑지 않겠다는 결정이 내려진 경우 확인 - - [ ] blackjackGame의 상태를 딜러의 턴 상태로 전환 +- [x] **마지막 플레이어의 턴과 딜러의 턴 상태 전환** + - [x] 마지막 플레이어의 턴에서 더 이상 카드를 뽑을 수 없거나, 뽑지 않겠다는 결정이 내려진 경우 확인 + - [x] blackjackGame의 상태를 딜러의 턴 상태로 전환 -- [ ] **딜러의 턴 상태 확인** - - [ ] blackjackGame이 딜러의 턴인 상태인지 확인 - - [ ] 딜러가 카드를 뽑을지 안 뽑을지 결정하고 사용자에게 알림 - - [ ] blackjackGame을 `ResultPage` 상태로 전환 +- [x] **딜러의 턴 상태 확인** + - [x] blackjackGame이 딜러의 턴인 상태인지 확인 + - [x] 딜러가 카드를 뽑을지 안 뽑을지 결정하고 사용자에게 알림 + - [x] blackjackGame을 `End` 상태로 전환 -- [ ] **ResultPage 상태 확인** - - [ ] blackjackGame이 ResultPage 상태인지 확인 - - [ ] blackjackGame에게 ResultReport를 요청하고 사용자에게 보여줌 +- [x] **End 상태 확인** + - [x] blackjackGame이 End 상태인지 확인 + - [x] blackjackGame에게 BlackjackResult를 요청하고 사용자에게 보여줌 -- [ ] **게임 종료** +- [x] **게임 종료** diff --git a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt index be54aa1b0a..fba04b359a 100644 --- a/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt +++ b/domain/src/main/kotlin/blackjack/game/BlackjackGame.kt @@ -49,28 +49,21 @@ class BlackjackGame private constructor( fun dealDealerTurn(): BlackJackAction { check(state is GameState.DealerTurn) { "Dealer Turn이 아닙니다." } - return when (dealer.decideAction(deck)) { - BlackJackAction.HIT -> { - state = GameState.End( - players, - dealer.receiveCard(deck.drawCard()) - ) - BlackJackAction.HIT - } - - BlackJackAction.STAND -> { - state = GameState.End(players, dealer) - BlackJackAction.STAND - } + val dealerAction = dealer.decideAction(deck) + return if (dealerAction == BlackJackAction.HIT) { + val drawnCard = deck.drawCard() + state = GameState.End(players, dealer.receiveCard(drawnCard)) + BlackJackAction.HIT + } else { + state = GameState.End(players, dealer) + BlackJackAction.STAND } } fun calculateResult(): Map { val results = mutableMapOf() results[dealer] = calculateDealerResult() - players.forEach { - results[it] = calculatePlayerResult(it) - } + players.forEach { results[it] = calculatePlayerResult(it) } return results } diff --git a/presenter/src/main/kotlin/ui/result/ResultView.kt b/presenter/src/main/kotlin/ui/result/ResultView.kt index 09a87dd12a..a11a930fc9 100644 --- a/presenter/src/main/kotlin/ui/result/ResultView.kt +++ b/presenter/src/main/kotlin/ui/result/ResultView.kt @@ -17,8 +17,8 @@ class ResultView { fun showCards(participant: BlackjackParticipant) { when (participant) { - is Dealer -> println("딜러 카드: ${participant.cards.joinToString(", ") { it.toUiString() }}") - is Player -> println("${participant.name} 카드: ${participant.cards.joinToString(", ") { it.toUiString() }}") + is Dealer -> println("딜러 카드: ${participant.cards.joinToString(", ") { it.toUiString() }} - 결과: ${participant.calculateBestValue()}") + is Player -> println("${participant.name} 카드: ${participant.cards.joinToString(", ") { it.toUiString() }} - 결과: ${participant.calculateBestValue()}") } } @@ -28,6 +28,7 @@ class ResultView { } fun showDealerTurn(dealerAction: BlackJackAction) { + println() val message = if (dealerAction == BlackJackAction.HIT) { "딜러는 16 이하라 1장 더 받습니다." } else { @@ -37,7 +38,7 @@ class ResultView { } fun showInitialDealMessage(players: List) { - println("딜러와 ${players.joinToString(", "){ it.name }}에게 2장의 카드를 나누었습니다.") + println("딜러와 ${players.joinToString(", ") { it.name }}에게 2장의 카드를 나누었습니다.") } fun showParticipantsRecord(blackjackGame: BlackjackGame, result: Map) { @@ -48,6 +49,7 @@ class ResultView { showPlayerRecord(it, result[it] ?: throw IllegalArgumentException()) } } + private fun showDealerRecord(dealerResult: BlackjackResult) { println("딜러: ${dealerResult.win}승 ${dealerResult.lose}패") }