diff --git a/README.md b/README.md index e046039d03..25bccbd783 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # kotlin-blackjack -## step2 +## step3 - [x] 초기 DeckCards 은 52장의 카드로 이루어진다 - [x] 초기 DeckCards 의 각 Suit 카드는 13장씩 있어야 한다 - [x] 초기 DeckCards 는 중복 없는 카드로 이루어져야 한다 @@ -17,3 +17,4 @@ - [x] Player 가 bust 상태가 되면 hit 할 수 없다 - [x] Player 가 blackjack 상태가 되면 hit 할 수 없다 - [x] Player 는 stay 후엔 다시 hit 할 수 없다 +- [x] Dealer 와 Player 의 승패가 정확히 결정된다 diff --git a/src/main/kotlin/blackjack/Blackjack.kt b/src/main/kotlin/blackjack/Blackjack.kt index e58e36b88a..a8b9984d32 100644 --- a/src/main/kotlin/blackjack/Blackjack.kt +++ b/src/main/kotlin/blackjack/Blackjack.kt @@ -1,11 +1,13 @@ package blackjack +import blackjack.domain.BlackjackResult +import blackjack.domain.PlayerResult import blackjack.domain.cards.Deck -import blackjack.domain.cards.HandCards -import blackjack.domain.player.Hand +import blackjack.domain.player.Dealer import blackjack.domain.player.Player import blackjack.domain.player.PlayerState import blackjack.view.InputView +import blackjack.view.InputViewCommand import blackjack.view.ResultView import blackjack.view.UserInputView @@ -13,15 +15,13 @@ class Blackjack( private val inputView: InputView, private val resultView: ResultView, ) { - private val deck = Deck.fullDeck() - - init { - deck.shuffle() - } + private val dealer = Dealer(Deck.fullDeck()) fun simulate() { val playerNames = inputView.getPlayerNames() + dealer.initHand() + val players = createPlayers(playerNames) resultView.printInitialState(players) @@ -30,26 +30,39 @@ class Blackjack( processPlayerTurn(player) } + dealer.processTurn { + resultView.printDealerTurn(it) + } + + resultView.printPlayer(dealer.asPlayer) resultView.printResult(players) + + val gameResult = BlackjackResult( + players.map { player -> + PlayerResult(player, dealer.wins(player)) + } + ) + + resultView.printBlackjackResult(gameResult) } private fun processPlayerTurn(player: Player) { while (player.state == PlayerState.Hit) { val command = inputView.getPlayerCommand(player.name) - player.play(command == "y") + player.play(command == InputViewCommand.Yes) + resultView.printPlayer(player) } } private fun createPlayers(playerNames: List): List { - return playerNames.map { Player(it, Hand(HandCards(mutableListOf(deck.draw(), deck.draw())))) } + return playerNames.map { Player(it, dealer.createInitialHand()) } } private fun Player.play(isHit: Boolean) { if (isHit) { hit() - val card = deck.draw() + val card = dealer.provideCard() addCard(card) - println("$name: ${hand.handCards}") } else { stay() } diff --git a/src/main/kotlin/blackjack/domain/BlackjackResult.kt b/src/main/kotlin/blackjack/domain/BlackjackResult.kt new file mode 100644 index 0000000000..2491da3c94 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackjackResult.kt @@ -0,0 +1,15 @@ +package blackjack.domain + +import blackjack.domain.player.Player + +data class BlackjackResult( + val playerResult: List +) { + val dealerWin get() = playerResult.count { it.isWin.not() } + val dealerLose get() = playerResult.count { it.isWin } +} + +data class PlayerResult( + val player: Player, + val isWin: Boolean, +) diff --git a/src/main/kotlin/blackjack/domain/player/Dealer.kt b/src/main/kotlin/blackjack/domain/player/Dealer.kt new file mode 100644 index 0000000000..4158c3355f --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Dealer.kt @@ -0,0 +1,42 @@ +package blackjack.domain.player + +import blackjack.domain.cards.Deck +import blackjack.domain.cards.HandCards + +class Dealer(private val deck: Deck) { + private lateinit var _player: Player + val asPlayer get() = _player + + init { + deck.shuffle() + } + + fun provideCard() = deck.draw() + + fun createInitialHand(): Hand = Hand(HandCards(mutableListOf(deck.draw(), deck.draw()))) + + fun initHand() { + _player = Player( + "딜러", + hand = createInitialHand() + ) + } + + fun processTurn(onResult: (Boolean) -> Unit) { + val addCard = _player.hand.valueSum() <= 16 + + if (addCard) _player.addCard(deck.draw()) + + onResult(addCard) + } + + fun wins(player: Player): Boolean { + return if (asPlayer.state.isBust()) { + false + } else if (asPlayer.state.isBust().not() && player.state.isBust()) { + true + } else { + asPlayer.hand.blackjackDiff() < player.hand.blackjackDiff() + } + } +} diff --git a/src/main/kotlin/blackjack/domain/player/Hand.kt b/src/main/kotlin/blackjack/domain/player/Hand.kt index b9ef10cf05..42d5e016ec 100644 --- a/src/main/kotlin/blackjack/domain/player/Hand.kt +++ b/src/main/kotlin/blackjack/domain/player/Hand.kt @@ -2,13 +2,17 @@ package blackjack.domain.player import blackjack.domain.card.Card import blackjack.domain.cards.HandCards +import kotlin.math.abs data class Hand(val handCards: HandCards) { fun addCard(card: Card) { handCards.add(card) } + fun valueSum(): Int = handCards.cardList.sumOf { it.character.value } + fun blackjackDiff() = abs(valueSum() - BLACK_JACK) + fun isBlackjack() = valueSum() == BLACK_JACK fun isBust() = valueSum() > BLACK_JACK diff --git a/src/main/kotlin/blackjack/domain/player/PlayerState.kt b/src/main/kotlin/blackjack/domain/player/PlayerState.kt index 9a4cb7cad2..0a1d7b5c41 100644 --- a/src/main/kotlin/blackjack/domain/player/PlayerState.kt +++ b/src/main/kotlin/blackjack/domain/player/PlayerState.kt @@ -1,5 +1,8 @@ package blackjack.domain.player enum class PlayerState { - Hit, Stay, Bust, Blackjack + Hit, Stay, Bust, Blackjack; + + fun isHit() = this == Hit + fun isBust() = this == Bust } diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt index 1f32a8e840..d467aaed12 100644 --- a/src/main/kotlin/blackjack/view/InputView.kt +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -3,5 +3,17 @@ package blackjack.view interface InputView { fun getPlayerNames(): List - fun getPlayerCommand(playerName: String): String + fun getPlayerCommand(playerName: String): InputViewCommand +} + +sealed interface InputViewCommand { + object Yes : InputViewCommand + object No : InputViewCommand + + companion object { + private val validYesCommands: Set = setOf("y") + fun get(commandString: String): InputViewCommand { + return if (commandString in validYesCommands) Yes else No + } + } } diff --git a/src/main/kotlin/blackjack/view/ResultView.kt b/src/main/kotlin/blackjack/view/ResultView.kt index 3439cb4854..6360e90a54 100644 --- a/src/main/kotlin/blackjack/view/ResultView.kt +++ b/src/main/kotlin/blackjack/view/ResultView.kt @@ -1,5 +1,6 @@ package blackjack.view +import blackjack.domain.BlackjackResult import blackjack.domain.player.Player class ResultView { @@ -16,7 +17,23 @@ class ResultView { } } - private fun printPlayer(player: Player) { + fun printPlayer(player: Player) { println("${player.name}: ${player.hand}") } + + fun printDealerTurn(addedCard: Boolean) { + if (addedCard) { + println("딜러는 16이하라 한장의 카드를 더 받았습니다.") + } else { + println("딜러는 17이상이라 카드를 받지 않았습니다.") + } + } + + fun printBlackjackResult(blackjackResult: BlackjackResult) { + println("## 최종 승패") + println("딜러: ${blackjackResult.dealerWin}승 ${blackjackResult.dealerLose}패") + blackjackResult.playerResult.forEach { playerResult -> + println("${playerResult.player.name}: ${if (playerResult.isWin) "승" else "패"}") + } + } } diff --git a/src/main/kotlin/blackjack/view/UserInputView.kt b/src/main/kotlin/blackjack/view/UserInputView.kt index 75ac6988e2..980e0966fe 100644 --- a/src/main/kotlin/blackjack/view/UserInputView.kt +++ b/src/main/kotlin/blackjack/view/UserInputView.kt @@ -6,8 +6,9 @@ class UserInputView : InputView { return readln().split(",").map { it.trim() } } - override fun getPlayerCommand(playerName: String): String { + override fun getPlayerCommand(playerName: String): InputViewCommand { println("${playerName}은 한 장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") - return readln().trim() + val cmd = readln().trim() + return InputViewCommand.get(cmd) } } diff --git a/src/test/kotlin/blackjack/domain/BlackjackResultTest.kt b/src/test/kotlin/blackjack/domain/BlackjackResultTest.kt new file mode 100644 index 0000000000..3861d4106f --- /dev/null +++ b/src/test/kotlin/blackjack/domain/BlackjackResultTest.kt @@ -0,0 +1,44 @@ +package blackjack.domain + +import blackjack.domain.card.Card +import blackjack.domain.card.Character +import blackjack.domain.card.Suit +import blackjack.domain.cards.HandCards +import blackjack.domain.player.Hand +import blackjack.domain.player.Player +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +private fun randomCard() = Card(Suit.values().random(), Character.values().random()) + +class BlackjackResultTest : StringSpec({ + "blackjackResult 테스트" { + val player1 = Player("a", Hand(HandCards(mutableListOf(randomCard(), randomCard())))) + val player2 = Player("b", Hand(HandCards(mutableListOf(randomCard(), randomCard())))) + val player3 = Player("c", Hand(HandCards(mutableListOf(randomCard(), randomCard())))) + val player4 = Player("d", Hand(HandCards(mutableListOf(randomCard(), randomCard())))) + val player5 = Player("e", Hand(HandCards(mutableListOf(randomCard(), randomCard())))) + + val playerResults = listOf( + PlayerResult( + player1, true + ), + PlayerResult( + player2, false + ), + PlayerResult( + player3, true + ), + PlayerResult( + player4, false + ), + PlayerResult( + player5, false + ), + ) + + val blackjackResult = BlackjackResult(playerResults) + blackjackResult.dealerWin shouldBe 3 + blackjackResult.dealerLose shouldBe 2 + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/DealerTest.kt b/src/test/kotlin/blackjack/domain/player/DealerTest.kt new file mode 100644 index 0000000000..56f4fc66b8 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/DealerTest.kt @@ -0,0 +1,77 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card +import blackjack.domain.card.Character +import blackjack.domain.card.Suit +import blackjack.domain.cards.Deck +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class DealerTest : StringSpec({ + "Dealer 는 Bust 패배한다" { + val dealer = Dealer(Deck.fullDeck()) + dealer.initHand() + + while (dealer.asPlayer.state.isHit()) { + dealer.asPlayer.addCard(dealer.provideCard()) + } + + val player = Player("pp", dealer.createInitialHand()) + + dealer.asPlayer.state.isBust() shouldBe true + dealer.wins(player) shouldBe false + } + + "Dealer 는 동점이면 패배한다" { + val dealer = Dealer(Deck.fullDeck()) + dealer.initHand() + + dealer.asPlayer.toTargetValueOver19(20) + + val player = Player("pp", dealer.createInitialHand()) + + player.toTargetValueOver19(20) + + dealer.asPlayer.hand.valueSum() shouldBe 20 + player.hand.valueSum() shouldBe 20 + + dealer.wins(player) shouldBe false + } + + "Dealer 승리 테스트" { + val dealer = Dealer(Deck.fullDeck()) + dealer.initHand() + + dealer.asPlayer.toTargetValueOver19(21) + + val player = Player("pp", dealer.createInitialHand()) + + player.toTargetValueOver19(20) + + dealer.asPlayer.hand.valueSum() shouldBe 21 + player.hand.valueSum() shouldBe 20 + + dealer.wins(player) shouldBe true + } + + "Dealer process turn 테스트" { + val dealer = Dealer(Deck.fullDeck()) + dealer.initHand() + + dealer.asPlayer.toTargetValueOver19(20) + + dealer.processTurn { + it shouldBe false + } + } +}) + +private fun Player.toTargetValueOver19(targetValue: Int) { + var remain = targetValue - hand.valueSum() + + while (remain != 0) { + val cardValue = if (remain > 10) remain - 10 else remain + addCard(Card(Suit.Diamond, Character.values().first { it.value == cardValue })) + remain -= cardValue + } +} diff --git a/src/test/kotlin/blackjack/domain/player/HandTest.kt b/src/test/kotlin/blackjack/domain/player/HandTest.kt index ff048de7bf..7c88cb1c30 100644 --- a/src/test/kotlin/blackjack/domain/player/HandTest.kt +++ b/src/test/kotlin/blackjack/domain/player/HandTest.kt @@ -46,4 +46,11 @@ class HandTest : StringSpec({ hand.isBlackjack() shouldBe false hand.isBust() shouldBe true } + + "blackjackDiff 테스트" { + val hand = Hand(HandCards(mutableListOf(Card(Suit.Spade, Character.Jack), Card(Suit.Clover, Character.Jack)))) + + hand.valueSum() shouldBe 20 + hand.blackjackDiff() shouldBe 1 + } })