diff --git a/README.md b/README.md index d1f319e5d6..a5323380e3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ - 카드를 더 줄 때마다 해당 player가 갖고있는 카드를 출력한다. - 모든 플레이어가 카드를 더 이상 받지 않으면 결과를 출력한다. - 결과는 플레이어별로 갖고 있는 카드의 목록과 점수를 출력한다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. +- 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리한다. +- 게임을 완료한 후 각 플레이어별로 승패를 출력한다. ## 테스트 구현 목록 - [x] Test 1 : Player 객체에 이름을 부여할 수 있다. @@ -32,4 +35,7 @@ - view라서 테스트 미구현 - [x] Test 4 : Player별 점수를 계산할 수 있다. - card의 리스트를 감싼 Cards 객체에게 점수 계산을 위임 +- [x] Test 5 : 딜러의 점수가 17점 이상이면 추가로 카드를 받을 수 없다. + - Dealer가 Player를 상속하도록 하고 addCard 로직을 바꿔서 구현 +- diff --git a/src/main/kotlin/blackjack/BlackJackGame.kt b/src/main/kotlin/blackjack/BlackJackGame.kt index f4ebfa458e..15b63f450e 100644 --- a/src/main/kotlin/blackjack/BlackJackGame.kt +++ b/src/main/kotlin/blackjack/BlackJackGame.kt @@ -1,6 +1,7 @@ package blackjack import blackjack.domain.Dealer +import blackjack.domain.Distributor import blackjack.domain.Player import blackjack.domain.Players import blackjack.domain.card.CardDeck @@ -11,34 +12,48 @@ class BlackJackGame { fun start() { val names = InputView.inputNameOfPlayer() - val players = Players(names) + val players = Players(names.map { Player(it) }) DisplayView.dealOutCards(players) - val dealer = Dealer(CardDeck()) - dealer.dealOutCards(players) - DisplayView.cardsOfPlayers(players) + val distributor = Distributor(CardDeck()) + val dealer = Dealer() - dealOutAdditionalCards(dealer, players) - DisplayView.result(players) + distributor.dealOutCards(dealer, players) + DisplayView.cardsOfPlayers(dealer, players) + + dealOutAdditionalCards(distributor, players) + dealOutAdditionalCard(distributor, dealer) + DisplayView.finalScore(dealer, players) + + GameResultCalculator.setResult(dealer, players) + DisplayView.result(dealer, players) } - private fun dealOutAdditionalCards(dealer: Dealer, players: Players) { - players.players.forEach { - dealOutAdditionalCard(dealer, it) + private fun dealOutAdditionalCards(distributor: Distributor, players: Players) { + players.forEach { + dealOutAdditionalCard(distributor, it) } } - private fun dealOutAdditionalCard(dealer: Dealer, player: Player) { + private fun dealOutAdditionalCard(distributor: Distributor, player: Player) { DisplayView.dealOutAdditionalCard(player) if (InputView.inputAdditionalCard() == "y") { - dealer.dealOutCard(player) - takeAnotherCard(dealer, player) + distributor.dealOutCard(player) + takeAnotherCard(distributor, player) + } + } + + private fun dealOutAdditionalCard(distributor: Distributor, dealer: Dealer) { + val received = dealer.isReceivableNewCard() + if (received) { + distributor.dealOutCard(dealer) } + DisplayView.dealOutAdditionalCard(received) } - private fun takeAnotherCard(dealer: Dealer, player: Player) { - if (player.getScore() >= MAX_SCORE) { - dealOutAdditionalCard(dealer, player) + private fun takeAnotherCard(distributor: Distributor, player: Player) { + if (player.isReceivableNewCard()) { + dealOutAdditionalCard(distributor, player) } } diff --git a/src/main/kotlin/blackjack/GameResult.kt b/src/main/kotlin/blackjack/GameResult.kt new file mode 100644 index 0000000000..ba4d1722eb --- /dev/null +++ b/src/main/kotlin/blackjack/GameResult.kt @@ -0,0 +1,6 @@ +package blackjack + +enum class GameResult(val description: String) { + WIN("승"), + LOSE("패") +} diff --git a/src/main/kotlin/blackjack/GameResultCalculator.kt b/src/main/kotlin/blackjack/GameResultCalculator.kt new file mode 100644 index 0000000000..be0321a71d --- /dev/null +++ b/src/main/kotlin/blackjack/GameResultCalculator.kt @@ -0,0 +1,18 @@ +package blackjack + +import blackjack.domain.Dealer +import blackjack.domain.Players + +object GameResultCalculator { + // 딜러와 각 플레이어의 점수를 비교해서 승패를 판별하고 기록함 + // 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리한다 + fun setResult(dealer: Dealer, players: Players) { + val scoreOfDealer = dealer.getScore() + val isDealerLose = scoreOfDealer > BlackJackGame.MAX_SCORE + players.forEach { + val scoreOfPlayer = it.getScore() + it.setGameResult(isDealerLose || scoreOfPlayer > scoreOfDealer) + dealer.setGameResult(!isDealerLose && scoreOfDealer > scoreOfPlayer) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Dealer.kt b/src/main/kotlin/blackjack/domain/Dealer.kt index 8f80f56162..820e0f2068 100644 --- a/src/main/kotlin/blackjack/domain/Dealer.kt +++ b/src/main/kotlin/blackjack/domain/Dealer.kt @@ -1,39 +1,34 @@ package blackjack.domain +import blackjack.GameResult import blackjack.domain.card.Card -import blackjack.domain.card.CardDeck -import kotlin.random.Random +import blackjack.domain.card.Cards -class Dealer( - private val cardDeck: CardDeck -) { +class Dealer(name: String, cards: Cards = Cards()) : Player(name, cards) { + private var isFinished = false + val gameResults = mutableListOf() - /** - * 플레이어들에게 2장씩 카드를 분배함 - */ - fun dealOutCards(players: Players) { - repeat(DEAL_OUT_CARD_AMOUNT){ - players.players.forEach{ dealOutCard(it) } + constructor(cards: Cards = Cards()) : this(DEALER_DISPLAY_NAME, cards) + + // 17점이 넘으면 호출되어도 더 이상 카드를 추가하지 않는다 + override fun addCard(card: Card) { + if (isReceivableNewCard()) { + super.addCard(card) + } else { + isFinished = true } } - /** - * 플레이어들에게 1장씩 카드를 분배함 - */ - fun dealOutCard(player: Player) { - player.cards.addCard(peekCard()) + override fun setGameResult(win: Boolean) { + gameResults.add(if (win) GameResult.WIN else GameResult.LOSE) } - /** - * 카드덱에서 랜덤한 카드를 1장 꺼냄 - */ - private fun peekCard(): Card { - val random = Random.Default - val randomIndex = random.nextInt(cardDeck.cards.size) - return cardDeck.peekCard(randomIndex) + override fun isReceivableNewCard(): Boolean { + return !isFinished && getScore() < LIMIT_SCORE } companion object { - const val DEAL_OUT_CARD_AMOUNT = 2 + private const val DEALER_DISPLAY_NAME = "딜러" + private const val LIMIT_SCORE = 17 } } diff --git a/src/main/kotlin/blackjack/domain/Distributor.kt b/src/main/kotlin/blackjack/domain/Distributor.kt new file mode 100644 index 0000000000..8a7f3e4029 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Distributor.kt @@ -0,0 +1,40 @@ +package blackjack.domain + +import blackjack.domain.card.Card +import blackjack.domain.card.CardDeck +import kotlin.random.Random + +class Distributor( + private val cardDeck: CardDeck +) { + + /** + * 딜러와 플레이어들에게 2장씩 카드를 분배함 + */ + fun dealOutCards(dealer: Dealer, players: Players) { + repeat(DEAL_OUT_CARD_AMOUNT) { + dealOutCard(dealer) + players.forEach { dealOutCard(it) } + } + } + + /** + * 플레이어들에게 1장씩 카드를 분배함 + */ + fun dealOutCard(player: Player) { + player.addCard(peekCard()) + } + + /** + * 카드덱에서 랜덤한 카드를 1장 꺼냄 + */ + private fun peekCard(): Card { + val random = Random.Default + val randomIndex = random.nextInt(cardDeck.cards.size) + return cardDeck.peekCard(randomIndex) + } + + companion object { + const val DEAL_OUT_CARD_AMOUNT = 2 + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt index 9880346b94..e8ff060e00 100644 --- a/src/main/kotlin/blackjack/domain/Player.kt +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -1,17 +1,33 @@ package blackjack.domain +import blackjack.BlackJackGame +import blackjack.GameResult +import blackjack.domain.card.Card import blackjack.domain.card.Cards -data class Player( +open class Player( val name: String, val cards: Cards = Cards() ) { + lateinit var gameResult: GameResult override fun toString(): String { return name } + open fun addCard(card: Card) { + cards.addCard(card) + } + fun getScore(): Int { return cards.getScore() } + + open fun isReceivableNewCard(): Boolean { + return getScore() < BlackJackGame.MAX_SCORE + } + + open fun setGameResult(win: Boolean) { + gameResult = if (win) GameResult.WIN else GameResult.LOSE + } } diff --git a/src/main/kotlin/blackjack/domain/Players.kt b/src/main/kotlin/blackjack/domain/Players.kt index 62752fe7d1..2e25403457 100644 --- a/src/main/kotlin/blackjack/domain/Players.kt +++ b/src/main/kotlin/blackjack/domain/Players.kt @@ -1,7 +1,3 @@ package blackjack.domain -data class Players( - val names: List -) { - val players: List = names.map { Player(it) } -} +data class Players(private val players: List) : List by players diff --git a/src/main/kotlin/blackjack/domain/card/Cards.kt b/src/main/kotlin/blackjack/domain/card/Cards.kt index 8ad24db9d4..34e315fe34 100644 --- a/src/main/kotlin/blackjack/domain/card/Cards.kt +++ b/src/main/kotlin/blackjack/domain/card/Cards.kt @@ -2,9 +2,7 @@ package blackjack.domain.card import blackjack.BlackJackGame -class Cards( - private val cards: MutableList = mutableListOf() -) { +class Cards(private val cards: MutableList = mutableListOf()) : MutableList by cards { fun addCard(card: Card) { cards += card } @@ -19,12 +17,11 @@ class Cards( } private fun isAceAvailable(score: Int): Boolean { - return cards.any { it.isAce() } - && (score + CardNumber.ACE_ADDITIONAL_SCORE) <= BlackJackGame.MAX_SCORE + return cards.any { it.isAce() } && + (score + CardNumber.ACE_ADDITIONAL_SCORE) <= BlackJackGame.MAX_SCORE } override fun toString(): String { return cards.joinToString() } - } diff --git a/src/main/kotlin/blackjack/view/DisplayView.kt b/src/main/kotlin/blackjack/view/DisplayView.kt index 1971081fa7..2dfd37b1dc 100644 --- a/src/main/kotlin/blackjack/view/DisplayView.kt +++ b/src/main/kotlin/blackjack/view/DisplayView.kt @@ -1,21 +1,28 @@ package blackjack.view +import blackjack.GameResult +import blackjack.domain.Dealer import blackjack.domain.Player import blackjack.domain.Players object DisplayView { fun dealOutCards(players: Players) { - val playersName = players.players.joinToString() - println("${playersName}에게 2장의 카드 나누었습니다.") + val playersName = players.joinToString() + println("딜러와 ${playersName}에게 2장의 카드 나누었습니다.") } - fun cardsOfPlayers(players: Players) { - players.players.forEach { + fun cardsOfPlayers(dealer: Dealer, players: Players) { + cardsOfDealer(dealer) + players.forEach { cardsOfPlayer(it) } } + fun cardsOfDealer(dealer: Dealer) { + println("${dealer.name}: ${dealer.cards}") + } + fun cardsOfPlayer(player: Player) { println("${player.name}카드: ${player.cards}") } @@ -24,10 +31,28 @@ object DisplayView { println("${player}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") } - fun result(players: Players) { - players.players.forEach { - println("${it.name}카드: ${it.cards} - 결과: ${it.getScore()}") + fun dealOutAdditionalCard(received: Boolean) { + if (received) { + println("딜러는 16이하라 한장의 카드를 더 받았습니다.") + } else { + println("딜러는 17이상이라 카드를 더 받지 않았습니다.") } } + fun finalScore(dealer: Dealer, players: Players) { + printFinalScore(dealer) + players.forEach { printFinalScore(it) } + } + + fun result(dealer: Dealer, players: Players) { + println("\n## 최종 승패") + val dealerWinCnt = dealer.gameResults.count { it == GameResult.WIN } + val dealerLoseCnt = dealer.gameResults.count { it == GameResult.LOSE } + println("딜러: ${dealerWinCnt}승 ${dealerLoseCnt}패") + players.forEach { println("${it.name}: ${it.gameResult.description}") } + } + + private fun printFinalScore(player: Player) { + println("${player.name}카드: ${player.cards} - 결과: ${player.getScore()}") + } } diff --git a/src/test/kotlin/blackjack/GameResultCalculatorTest.kt b/src/test/kotlin/blackjack/GameResultCalculatorTest.kt new file mode 100644 index 0000000000..37c0ead0c1 --- /dev/null +++ b/src/test/kotlin/blackjack/GameResultCalculatorTest.kt @@ -0,0 +1,53 @@ +package blackjack + +import blackjack.domain.Dealer +import blackjack.domain.Player +import blackjack.domain.Players +import blackjack.domain.card.Card +import blackjack.domain.card.CardNumber +import blackjack.domain.card.CardSymbol +import blackjack.domain.card.Cards +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class GameResultCalculatorTest : StringSpec({ + "딜러가 21점을 넘어가면 남아 있던 플레이어들은 점수에 관계 없이 승리한다" { + val dealer = Dealer( + "dealer", + Cards( + mutableListOf( + Card(CardNumber.JACK, CardSymbol.HEART), + Card(CardNumber.QUEEN, CardSymbol.HEART), + Card(CardNumber.KING, CardSymbol.HEART) + ) + ) + ) + val playerUnderMaxScore = Player( + "playerUnderMaxScore", + Cards( + mutableListOf( + Card(CardNumber.ACE, CardSymbol.HEART), + Card(CardNumber.SIX, CardSymbol.HEART), + Card(CardNumber.NINE, CardSymbol.HEART) + ) + ) + ) + val playerOverMaxScore = Player( + "playerOverMaxScore", + Cards( + mutableListOf( + Card(CardNumber.JACK, CardSymbol.HEART), + Card(CardNumber.QUEEN, CardSymbol.HEART), + Card(CardNumber.KING, CardSymbol.HEART) + ) + ) + ) + val players = Players(listOf(playerUnderMaxScore, playerOverMaxScore)) + + GameResultCalculator.setResult(dealer, players) + + players.forEach { + it.gameResult shouldBe GameResult.WIN + } + } +}) diff --git a/src/test/kotlin/blackjack/PlayerTest.kt b/src/test/kotlin/blackjack/PlayerTest.kt index 874f79b502..a4a6c153ec 100644 --- a/src/test/kotlin/blackjack/PlayerTest.kt +++ b/src/test/kotlin/blackjack/PlayerTest.kt @@ -20,10 +20,10 @@ class PlayerTest : StringSpec({ "쉼표 기준으로 분리된 이름을 받아 Player들을 생성한다" { val nameList = listOf("pobi", "jason") - val players = Players(nameList) + val players = Players(nameList.map { Player(it) }) nameList.all { name -> - players.players.any { player -> + players.any { player -> player.name == name } } shouldBe true diff --git a/src/test/kotlin/blackjack/domain/DealerTest.kt b/src/test/kotlin/blackjack/domain/DealerTest.kt new file mode 100644 index 0000000000..c31b7e8996 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/DealerTest.kt @@ -0,0 +1,38 @@ +package blackjack.domain + +import blackjack.domain.card.Card +import blackjack.domain.card.CardNumber +import blackjack.domain.card.CardSymbol +import blackjack.domain.card.Cards +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.shouldBe + +class DealerTest : StringSpec({ + "17점이상의 카드를 가진 딜러는 카드를 받지 않는다" { + val cardsUnder17 = Cards(mutableListOf(Card(CardNumber.SIX, CardSymbol.DIAMOND), Card(CardNumber.JACK, CardSymbol.DIAMOND))) + val cardsScore17 = Cards(mutableListOf(Card(CardNumber.SEVEN, CardSymbol.HEART), Card(CardNumber.JACK, CardSymbol.HEART))) + val cardsOver17 = Cards(mutableListOf(Card(CardNumber.EIGHT, CardSymbol.SPADE), Card(CardNumber.JACK, CardSymbol.SPADE))) + + val firstDealer = Dealer("cardsUnder17", cardsUnder17) + val scoreOfFirstDealer = firstDealer.getScore() + + val secondDealer = Dealer("cardsScore17", cardsScore17) + val scoreOfSecondDealer = secondDealer.getScore() + + val thirdDealer = Dealer("cardsOver17", cardsOver17) + val scoreOfThirdDealer = thirdDealer.getScore() + + val additionalCard = Card(CardNumber.ACE, CardSymbol.CLOVER) + + // when + firstDealer.addCard(additionalCard) + secondDealer.addCard(additionalCard) + thirdDealer.addCard(additionalCard) + + // then + firstDealer.getScore() shouldBeGreaterThan scoreOfFirstDealer + secondDealer.getScore() shouldBe scoreOfSecondDealer + thirdDealer.getScore() shouldBe scoreOfThirdDealer + } +}) diff --git a/src/test/kotlin/blackjack/domain/DistributorTest.kt b/src/test/kotlin/blackjack/domain/DistributorTest.kt new file mode 100644 index 0000000000..5ceaa290c8 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/DistributorTest.kt @@ -0,0 +1,21 @@ +package blackjack.domain + +import blackjack.domain.card.CardDeck +import blackjack.domain.card.Cards +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class DistributorTest : StringSpec({ + "딜러와 플레이어들에 카드가 2장씩 분배 되었는지" { + val dealer = Dealer("dealer", Cards(mutableListOf())) + val player1 = Player("player1", Cards(mutableListOf())) + val player2 = Player("player2", Cards(mutableListOf())) + val players = Players(listOf(player1, player2)) + + val distributor = Distributor(CardDeck()) + distributor.dealOutCards(dealer, players) + + dealer.cards.size shouldBe 2 + players.forEach { player -> player.cards.size shouldBe 2 } + } +})