Skip to content

Commit

Permalink
[Feat] 블랙잭에 딜러 클래스를 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
YiBeomSeok committed Nov 24, 2023
1 parent 4b7a27d commit b3acbfb
Show file tree
Hide file tree
Showing 16 changed files with 215 additions and 53 deletions.
8 changes: 7 additions & 1 deletion docs/BLACKJACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,10 @@ jason카드: 7클로버, K스페이드 - 결과: 17

- [ ] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다.
- [ ] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다.
- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다.
- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다.

## 딜러가 추가된 사전 구현 계획

- [ ] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
- [x] 만일 딜러가 <Ace, 6>을 갖고 있다면?
- [x] 딜러는 다양한 전략을 사용할 수 있도록 한다.
6 changes: 6 additions & 0 deletions domain/src/main/kotlin/action/BlackJackAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package action

enum class BlackJackAction {
HIT,
STAND,
}
10 changes: 10 additions & 0 deletions domain/src/main/kotlin/blackjack/BlackjackParticipant.kt
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 24 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/Dealer.kt
Original file line number Diff line number Diff line change
@@ -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<Card>
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)
}
}
9 changes: 9 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
3 changes: 3 additions & 0 deletions domain/src/main/kotlin/blackjack/deck/Deck.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Deck(
addAll(cardShuffler.shuffle(cardProvider.provideCards()))
}

val remainingCards: List<Card>
get() = cards.toList()

val size
get() = cards.size

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package blackjack.deck

import blackjack.card.Card

class RandomCardShuffler : CardShuffler {
internal class RandomCardShuffler : CardShuffler {
override fun shuffle(cards: List<Card>): List<Card> = cards.shuffled()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Card> =
CardSuit.values().flatMap { suit ->
CardRank.values().map { rank -> Card(suit, rank) }
Expand Down
36 changes: 5 additions & 31 deletions domain/src/main/kotlin/blackjack/hand/Hand.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,10 @@
package blackjack.hand

import blackjack.card.Card
import blackjack.card.CardRank

data class Hand(
val cards: List<Card> = 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<Card>
fun addCard(card: Card): Hand
fun calculateMinValue(): Int
fun calculateBestValue(): Int
}
50 changes: 50 additions & 0 deletions domain/src/main/kotlin/blackjack/hand/StandardHand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package blackjack.hand

import blackjack.card.Card
import blackjack.card.CardRank

internal class StandardHand(
override val cards: Set<Card> = 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
}
}
21 changes: 15 additions & 6 deletions domain/src/main/kotlin/blackjack/player/Player.kt
Original file line number Diff line number Diff line change
@@ -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<Card>
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()
}
37 changes: 37 additions & 0 deletions domain/src/test/kotlin/blackjack/dealer/DealerTest.kt
Original file line number Diff line number Diff line change
@@ -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("ACEJACK을 가지고 있을 때, 베스트는 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
}
})
8 changes: 4 additions & 4 deletions domain/src/test/kotlin/blackjack/hand/HandTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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),
Expand Down
12 changes: 6 additions & 6 deletions domain/src/test/kotlin/blackjack/player/PlayerTest.kt
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions presenter/src/main/kotlin/ui/Main.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Expand All @@ -29,7 +29,7 @@ fun playBlackjack(deck: Deck, players: List<Player>, 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
}
Expand Down

0 comments on commit b3acbfb

Please sign in to comment.