diff --git a/README.md b/README.md index dd6e2bb9a..7817229dc 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,25 @@ - [x] 덱에서 카드를 하나 꺼낸다. - [x] 유저는 카드 목록을 보유한다. - [x] 카드목록에서 점수를 계산한다. -- [x] 카드를 덱에서 하나 뽑는다. \ No newline at end of file +- [x] 카드를 덱에서 하나 뽑는다. + +## Step3 - 블랙잭(딜러) + +### Step2 - 리뷰반영사항 +- [x] BlackJackGame::game table 이 users 와 deck 을 상태로서 관리하도록 수정 +- [x] BlackJackGame::start 메서드 분리 +- [x] BlackJackGame::유저목록을 받아 card 출력하도록 수정 +- [x] BlackJackGame::카드 히트 여부확인시 출력과 입력을 통합 +- [x] BlackJackGame::while 문 내부 if 절을 while 조건식으로 통합 +- [x] BlackJackGame::게임진행책임 분리 +- [x] BlackJackGame::receive 를 hit 로 메서드명 수정 +- [x] Rank::enum 클래스로 수정 + +### 기능 구현사항 +- [x] 딜러는 처음받는 2장의 합계가 16이하면 반드시 1장의 카드를 추가로 받는다 +- [x] 딜러가 21을 초과하면 남은 플레이어들은 패에 상관없이 승리한다 +- [x] 게임 완료 후 각 플레이어별로 승패를 출력한다 +- [x] data class , 일반 클래스 설정 일관성 유지 +- [x] 도메인 패키지 분리 +- [x] 객체 상태를 객체가 관리 +- [x] 게임 관련 로직 분리 \ No newline at end of file diff --git a/src/main/kotlin/blackjack/Main.kt b/src/main/kotlin/blackjack/Main.kt index 865246b0a..c857254f0 100644 --- a/src/main/kotlin/blackjack/Main.kt +++ b/src/main/kotlin/blackjack/Main.kt @@ -1,10 +1,7 @@ package blackjack -import blackjack.controller.BlackJackGame -import blackjack.domain.GameTable -import blackjack.view.InputView -import blackjack.view.ResultView +import blackjack.controller.BlackjackGame fun main() { - BlackJackGame(GameTable, InputView, ResultView).start() + BlackjackGame.start() } diff --git a/src/main/kotlin/blackjack/controller/BlackJackGame.kt b/src/main/kotlin/blackjack/controller/BlackJackGame.kt deleted file mode 100644 index 3595c1016..000000000 --- a/src/main/kotlin/blackjack/controller/BlackJackGame.kt +++ /dev/null @@ -1,50 +0,0 @@ -package blackjack.controller - -import blackjack.domain.Deck -import blackjack.domain.GameTable -import blackjack.domain.User -import blackjack.view.InputView -import blackjack.view.ResultView - -class BlackJackGame( - private val gameTable: GameTable, - private val inputView: InputView, - private val resultView: ResultView, -) { - fun start() { - val names = inputView.inputNames() - val deck = Deck.create() - val users = names.map { User.create(name = it) } - - val initCardReceivedUsers = gameTable.dealInitCard(users, deck) - - println() - resultView.printInitCardReceive(initCardReceivedUsers) - initCardReceivedUsers.forEach { resultView.printUserCards(user = it, printScore = false) } - - val allCardReceivedUsers = - initCardReceivedUsers.map { user -> - var currentUser = user - while (true) { - if (!currentUser.canReceiveCard()) { - resultView.printCanNotReceivedCard() - break - } - resultView.printAskReceiveMoreCard(currentUser) - val moreCard = inputView.inputReceiveMoreCard() - if (moreCard) { - currentUser = currentUser.receiveCard(deck.draw()) - resultView.printUserCards(user = currentUser, printScore = false) - } else { - break - } - } - currentUser - } - - println() - allCardReceivedUsers.forEach { user -> - resultView.printUserCards(user = user, printScore = true) - } - } -} diff --git a/src/main/kotlin/blackjack/controller/BlackjackGame.kt b/src/main/kotlin/blackjack/controller/BlackjackGame.kt new file mode 100644 index 000000000..c090e01c1 --- /dev/null +++ b/src/main/kotlin/blackjack/controller/BlackjackGame.kt @@ -0,0 +1,35 @@ +package blackjack.controller + +import blackjack.domain.card.Deck +import blackjack.domain.player.Dealer +import blackjack.domain.player.Player +import blackjack.view.InputView +import blackjack.view.ResultView + +object BlackjackGame { + fun start() { + val gameTable = setUp() + initDeal(gameTable) + turnStart(gameTable) + ResultView.printAfterTurn(gameTable) + } + + private fun setUp(): GameTable { + val gameTable = GameTable(Deck(), Dealer(), getPlayers()) + ResultView.linebreak() + return gameTable + } + + private fun getPlayers(): List<Player> = InputView.inputNames().map { Player(it) } + + private fun initDeal(gameTable: GameTable) { + gameTable.dealInitCard() + ResultView.printDealInitCard(gameTable) + } + + private fun turnStart(gameTable: GameTable) { + gameTable.playersTurn() + ResultView.linebreak() + gameTable.dealerTurn() + } +} diff --git a/src/main/kotlin/blackjack/controller/GameTable.kt b/src/main/kotlin/blackjack/controller/GameTable.kt new file mode 100644 index 000000000..4fa31a914 --- /dev/null +++ b/src/main/kotlin/blackjack/controller/GameTable.kt @@ -0,0 +1,46 @@ +package blackjack.controller + +import blackjack.domain.card.Deck +import blackjack.domain.game.GameResult +import blackjack.domain.player.Dealer +import blackjack.domain.player.Player +import blackjack.view.InputView +import blackjack.view.ResultView + +class GameTable( + val deck: Deck, + val dealer: Dealer, + val players: List<Player>, +) { + fun dealInitCard() = + repeat(INIT_CARD_DRAW_COUNT) { + dealer.hit(deck.draw()) + players.forEach { it.hit(deck.draw()) } + } + + fun playersTurn() = players.forEach { playerTurn(it) } + + fun dealerTurn() { + if (!dealer.canHit()) { + return + } + ResultView.printDealerHit() + dealer.hit(deck.draw()) + dealerTurn() + } + + fun getGameResult(): GameResult = GameResult.from(dealer, players) + + private fun playerTurn(player: Player) { + if (!player.canHit() || !InputView.inputHit(player)) { + return + } + player.hit(deck.draw()) + ResultView.printPlayerCard(player) + playerTurn(player) + } + + companion object { + const val INIT_CARD_DRAW_COUNT = 2 + } +} diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt deleted file mode 100644 index c648d9a78..000000000 --- a/src/main/kotlin/blackjack/domain/Card.kt +++ /dev/null @@ -1,19 +0,0 @@ -package blackjack.domain - -import blackjack.domain.Rank.Companion.ACE - -data class Card( - val rank: Rank, - val suit: Suit, -) { - val score = rank.score - - fun isAce(): Boolean { - return rank == ACE - } - - companion object { - val ALL: List<Card> = - Suit.entries.flatMap { suit -> Rank.ALL.map { rank -> Card(rank, suit) } } - } -} diff --git a/src/main/kotlin/blackjack/domain/Deck.kt b/src/main/kotlin/blackjack/domain/Deck.kt deleted file mode 100644 index d6b33e27c..000000000 --- a/src/main/kotlin/blackjack/domain/Deck.kt +++ /dev/null @@ -1,14 +0,0 @@ -package blackjack.domain - -data class Deck(private val cards: MutableList<Card>) { - fun draw(): Card { - check(cards.isNotEmpty()) { "카드가 모두 소진되었습니다" } - return cards.removeFirst() - } - - companion object { - fun create(): Deck { - return Deck(Card.ALL.shuffled().toMutableList()) - } - } -} diff --git a/src/main/kotlin/blackjack/domain/GameTable.kt b/src/main/kotlin/blackjack/domain/GameTable.kt deleted file mode 100644 index c56f577b2..000000000 --- a/src/main/kotlin/blackjack/domain/GameTable.kt +++ /dev/null @@ -1,16 +0,0 @@ -package blackjack.domain - -object GameTable { - const val INIT_CARD_DRAW_COUNT = 2 - - fun dealInitCard( - users: List<User>, - deck: Deck, - ): List<User> { - return users.map { user -> - (1..INIT_CARD_DRAW_COUNT).fold(user) { acc, _ -> - acc.receiveCard(deck.draw()) - } - } - } -} diff --git a/src/main/kotlin/blackjack/domain/Rank.kt b/src/main/kotlin/blackjack/domain/Rank.kt deleted file mode 100644 index 67049a95b..000000000 --- a/src/main/kotlin/blackjack/domain/Rank.kt +++ /dev/null @@ -1,31 +0,0 @@ -package blackjack.domain - -@JvmInline -value class Rank(val value: String) { - init { - require(value in RANK_VALUES) { "랭크는 $RANK_VALUES 에 포함되어야 합니다" } - } - - val score: Int - get() = - when (value) { - in NUMBER_VALUES -> value.toInt() - in FACE_VALUES -> FACE_SCORE - ACE_VALUE -> DEFAULT_ACE_SCORE - else -> throw IllegalArgumentException("유효하지 않은 랭크 값입니다") - } - - companion object { - private const val FACE_SCORE = 10 - private const val DEFAULT_ACE_SCORE = 11 - - private const val ACE_VALUE = "A" - private val NUMBER_VALUES = listOf("2", "3", "4", "5", "6", "7", "8", "9", "10") - private val FACE_VALUES = listOf("J", "Q", "K") - - private val RANK_VALUES = NUMBER_VALUES + FACE_VALUES + ACE_VALUE - - val ACE = Rank(ACE_VALUE) - val ALL = RANK_VALUES.map { Rank(it) } - } -} diff --git a/src/main/kotlin/blackjack/domain/User.kt b/src/main/kotlin/blackjack/domain/User.kt deleted file mode 100644 index ceab91f53..000000000 --- a/src/main/kotlin/blackjack/domain/User.kt +++ /dev/null @@ -1,20 +0,0 @@ -package blackjack.domain - -data class User( - val name: String, - val cards: Cards, -) { - fun canReceiveCard(): Boolean { - return cards.isScoreLowerThanLimit() - } - - fun receiveCard(card: Card): User { - return this.copy(cards = cards.add(card)) - } - - companion object { - fun create(name: String): User { - return User(name, Cards(emptyList())) - } - } -} diff --git a/src/main/kotlin/blackjack/domain/card/Card.kt b/src/main/kotlin/blackjack/domain/card/Card.kt new file mode 100644 index 000000000..49e38e89d --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/Card.kt @@ -0,0 +1,19 @@ +package blackjack.domain.card + +import blackjack.domain.card.Rank.ACE + +data class Card( + val rank: Rank, + val suit: Suit, +) { + val score: Int + get() = rank.score + + val isAce: Boolean + get() = rank == ACE + + companion object { + val ALL: List<Card> = + Suit.entries.flatMap { suit -> Rank.entries.map { rank -> Card(rank, suit) } } + } +} diff --git a/src/main/kotlin/blackjack/domain/card/Deck.kt b/src/main/kotlin/blackjack/domain/card/Deck.kt new file mode 100644 index 000000000..6f20fd3ce --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/Deck.kt @@ -0,0 +1,8 @@ +package blackjack.domain.card + +class Deck(private val cards: MutableList<Card> = Card.ALL.shuffled().toMutableList()) { + fun draw(): Card { + check(cards.isNotEmpty()) { "카드가 모두 소진되었습니다" } + return cards.removeFirst() + } +} diff --git a/src/main/kotlin/blackjack/domain/Cards.kt b/src/main/kotlin/blackjack/domain/card/Hand.kt similarity index 58% rename from src/main/kotlin/blackjack/domain/Cards.kt rename to src/main/kotlin/blackjack/domain/card/Hand.kt index 248a97f8b..bebed6e7d 100644 --- a/src/main/kotlin/blackjack/domain/Cards.kt +++ b/src/main/kotlin/blackjack/domain/card/Hand.kt @@ -1,20 +1,19 @@ -package blackjack.domain +package blackjack.domain.card + +class Hand(private val _cards: MutableList<Card> = mutableListOf()) { + val cards: List<Card> + get() = _cards.toList() -data class Cards(val values: List<Card>) { val score: Int get() = calculateScore() - fun isScoreLowerThanLimit(): Boolean { - return score < BLACKJACK_SCORE_LIMIT - } - - fun add(card: Card): Cards { - return Cards(values + card) + fun add(card: Card) { + _cards.add(card) } private fun calculateScore(): Int { - val totalScore = values.sumOf { it.score } - var aceCount = values.count { it.isAce() } + val totalScore = cards.sumOf { it.score } + var aceCount = cards.count { it.isAce } var adjustedScore = totalScore while (adjustedScore > BLACKJACK_SCORE_LIMIT && aceCount > 0) { diff --git a/src/main/kotlin/blackjack/domain/card/Rank.kt b/src/main/kotlin/blackjack/domain/card/Rank.kt new file mode 100644 index 000000000..7b7e1ad82 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/Rank.kt @@ -0,0 +1,24 @@ +package blackjack.domain.card + +enum class Rank(val value: String, val score: Int) { + ACE("A", 11), + TWO("2", 2), + THREE("3", 3), + FOUR("4", 4), + FIVE("5", 5), + SIX("6", 6), + SEVEN("7", 7), + EIGHT("8", 8), + NINE("9", 9), + TEN("10", 10), + JACK("J", 10), + QUEEN("Q", 10), + KING("K", 10), + ; + + companion object { + fun from(value: String): Rank = + entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("유효하지 않은 랭크 값입니다: $value") + } +} diff --git a/src/main/kotlin/blackjack/domain/Suit.kt b/src/main/kotlin/blackjack/domain/card/Suit.kt similarity index 82% rename from src/main/kotlin/blackjack/domain/Suit.kt rename to src/main/kotlin/blackjack/domain/card/Suit.kt index bc2d994b1..fd563f625 100644 --- a/src/main/kotlin/blackjack/domain/Suit.kt +++ b/src/main/kotlin/blackjack/domain/card/Suit.kt @@ -1,4 +1,4 @@ -package blackjack.domain +package blackjack.domain.card enum class Suit(val description: String) { SPADE("스페이드"), diff --git a/src/main/kotlin/blackjack/domain/game/GameResult.kt b/src/main/kotlin/blackjack/domain/game/GameResult.kt new file mode 100644 index 000000000..cf7856903 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/game/GameResult.kt @@ -0,0 +1,31 @@ +package blackjack.domain.game + +import blackjack.domain.game.MatchResult.DRAW +import blackjack.domain.game.MatchResult.LOSE +import blackjack.domain.game.MatchResult.WIN +import blackjack.domain.game.dto.DealerGameResult +import blackjack.domain.game.dto.PlayerGameResult +import blackjack.domain.player.Dealer +import blackjack.domain.player.Player + +data class GameResult( + val dealerGameResult: DealerGameResult, + val playerGameResults: List<PlayerGameResult>, +) { + companion object { + fun from( + dealer: Dealer, + players: List<Player>, + ): GameResult { + val playerGameResults = players.map { player -> PlayerGameResult(player, player.matchHand(dealer)) } + return GameResult( + DealerGameResult( + winCount = playerGameResults.count { it.result == LOSE }, + loseCount = playerGameResults.count { it.result == WIN }, + drawCount = playerGameResults.count { it.result == DRAW }, + ), + playerGameResults, + ) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/game/MatchResult.kt b/src/main/kotlin/blackjack/domain/game/MatchResult.kt new file mode 100644 index 000000000..fd60f033b --- /dev/null +++ b/src/main/kotlin/blackjack/domain/game/MatchResult.kt @@ -0,0 +1,7 @@ +package blackjack.domain.game + +enum class MatchResult(val description: String) { + WIN("승"), + LOSE("패"), + DRAW("무"), +} diff --git a/src/main/kotlin/blackjack/domain/game/dto/DealerGameResult.kt b/src/main/kotlin/blackjack/domain/game/dto/DealerGameResult.kt new file mode 100644 index 000000000..aa160e028 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/game/dto/DealerGameResult.kt @@ -0,0 +1,7 @@ +package blackjack.domain.game.dto + +data class DealerGameResult( + val winCount: Int, + val loseCount: Int, + val drawCount: Int, +) diff --git a/src/main/kotlin/blackjack/domain/game/dto/PlayerGameResult.kt b/src/main/kotlin/blackjack/domain/game/dto/PlayerGameResult.kt new file mode 100644 index 000000000..40e60dfc9 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/game/dto/PlayerGameResult.kt @@ -0,0 +1,9 @@ +package blackjack.domain.game.dto + +import blackjack.domain.game.MatchResult +import blackjack.domain.player.Player + +data class PlayerGameResult( + val player: Player, + val result: MatchResult, +) 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 000000000..c8d958b08 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Dealer.kt @@ -0,0 +1,16 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card + +class Dealer : Player( + DEALER_NAME, +) { + override fun canHit(): Boolean = hand.score < DEALER_SCORE_LIMIT + + override fun hit(card: Card) = hand.add(card) + + companion object { + private const val DEALER_NAME = "딜러" + private const val DEALER_SCORE_LIMIT = 17 + } +} diff --git a/src/main/kotlin/blackjack/domain/player/Player.kt b/src/main/kotlin/blackjack/domain/player/Player.kt new file mode 100644 index 000000000..ff465d03c --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Player.kt @@ -0,0 +1,42 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card +import blackjack.domain.card.Hand +import blackjack.domain.game.MatchResult +import blackjack.domain.game.MatchResult.DRAW +import blackjack.domain.game.MatchResult.LOSE +import blackjack.domain.game.MatchResult.WIN + +open class Player( + val name: String, + val hand: Hand = Hand(), +) { + open fun canHit(): Boolean = hand.score < PLAYER_SCORE_LIMIT + + open fun hit(card: Card) = hand.add(card) + + fun matchHand(other: Player): MatchResult = + when { + other.isBust() -> WIN + this.isBust() -> LOSE + other.isBlackjack() && this.isBlackjack() -> DRAW + other.isBlackjack() -> LOSE + this.isBlackjack() -> WIN + else -> compareScore(other) + } + + private fun compareScore(other: Player): MatchResult = + when { + this.hand.score > other.hand.score -> WIN + this.hand.score < other.hand.score -> LOSE + else -> DRAW + } + + private fun isBust(): Boolean = hand.score > PLAYER_SCORE_LIMIT + + private fun isBlackjack(): Boolean = hand.score == PLAYER_SCORE_LIMIT + + companion object { + private const val PLAYER_SCORE_LIMIT = 21 + } +} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt index 12bbe7f90..79b34c263 100644 --- a/src/main/kotlin/blackjack/view/InputView.kt +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -1,5 +1,7 @@ package blackjack.view +import blackjack.domain.player.Player + object InputView { fun inputNames(): List<String> { println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") @@ -10,7 +12,8 @@ object InputView { return names } - fun inputReceiveMoreCard(): Boolean { + fun inputHit(player: Player): Boolean { + println("${player.name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") return when (readlnOrNull()) { "y" -> true "n" -> false diff --git a/src/main/kotlin/blackjack/view/ResultView.kt b/src/main/kotlin/blackjack/view/ResultView.kt index 56430ad2c..80ec6a4d4 100644 --- a/src/main/kotlin/blackjack/view/ResultView.kt +++ b/src/main/kotlin/blackjack/view/ResultView.kt @@ -1,27 +1,57 @@ package blackjack.view -import blackjack.domain.GameTable.INIT_CARD_DRAW_COUNT -import blackjack.domain.User +import blackjack.controller.GameTable +import blackjack.controller.GameTable.Companion.INIT_CARD_DRAW_COUNT +import blackjack.domain.player.Player object ResultView { - fun printInitCardReceive(users: List<User>) { - println("${users.joinToString(", ") { it.name }}에게 ${INIT_CARD_DRAW_COUNT}장의 카드를 나누었습니다.") + fun printDealerHit() = println("딜러는 16이하라 한장의 카드를 더 받았습니다.") + + fun printDealInitCard(gameTable: GameTable) { + println("딜러와 ${gameTable.players.joinToString(", ") { it.name }}에게 ${INIT_CARD_DRAW_COUNT}장의 카드를 나누었습니다.") + printPlayerCard(gameTable.dealer) + printPlayersCard(gameTable.players) + linebreak() + } + + fun printAfterTurn(gameTable: GameTable) { + printFinalHand(gameTable) + printGameResult(gameTable) } - fun printUserCards( - user: User, - printScore: Boolean, + fun printPlayerCard( + player: Player, + printScore: Boolean = false, ) { - val cards = user.cards.values.joinToString(", ") { "${it.rank.value}${it.suit.description}" } - val scoreText = "- 결과: ${user.cards.score}" - println("${user.name}카드: $cards ${if (printScore) scoreText else ""}") + val cards = player.hand.cards.joinToString(", ") { "${it.rank.value}${it.suit.description}" } + val scoreText = "- 결과: ${player.hand.score}" + println("${player.name} 카드: $cards ${if (printScore) scoreText else ""}") } - fun printAskReceiveMoreCard(user: User) { - println("${user.name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + fun linebreak() = println() + + private fun printFinalHand(gameTable: GameTable) { + linebreak() + printPlayerCard(gameTable.dealer, printScore = true) + printPlayersCard(gameTable.players, printScore = true) } - fun printCanNotReceivedCard() { - println("더 이상 카드를 받을 수 없습니다") + private fun printGameResult(gameTable: GameTable) { + linebreak() + val gameResult = gameTable.getGameResult() + val winMessage = + if (gameResult.dealerGameResult.winCount > 0) "${gameResult.dealerGameResult.winCount}승" else "" + val lossMessage = + if (gameResult.dealerGameResult.loseCount > 0) "${gameResult.dealerGameResult.loseCount}패" else "" + val drawMessage = + if (gameResult.dealerGameResult.drawCount > 0) "${gameResult.dealerGameResult.drawCount}무" else "" + println("## 최종 승패") + println("딜러: $winMessage $lossMessage $drawMessage") + gameResult.playerGameResults.forEach { println("${it.player.name}: ${it.result.description}") } } + + private fun printPlayersCard( + players: List<Player>, + printScore: Boolean = false, + ) = players.forEach { printPlayerCard(it, printScore) } } diff --git a/src/test/kotlin/blackjack/domain/CardTest.kt b/src/test/kotlin/blackjack/domain/CardTest.kt deleted file mode 100644 index 0a22b88f9..000000000 --- a/src/test/kotlin/blackjack/domain/CardTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package blackjack.domain - -import blackjack.domain.Suit.SPADE -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class CardTest : StringSpec({ - "카드는 에이스로 만들어진다면 에이스 카드이다" { - val card = Card(Rank("A"), SPADE) - - card.isAce() shouldBe true - } - - "카드는 스페이드로 만들어진다면 에이스 카드가 아니다" { - val card = Card(Rank("3"), SPADE) - - card.isAce() shouldBe false - } -}) diff --git a/src/test/kotlin/blackjack/domain/DeckTest.kt b/src/test/kotlin/blackjack/domain/DeckTest.kt deleted file mode 100644 index bbcd5afbc..000000000 --- a/src/test/kotlin/blackjack/domain/DeckTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package blackjack.domain - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldNotBe - -class DeckTest : StringSpec({ - "덱에서 카드를 하나 꺼낸다" { - val deck = Deck.create() - val card = deck.draw() - - card shouldNotBe null - } - - "덱에 카드가 없을 때 카드를 꺼내면 예외 발생한다" { - val deck = Deck.create() - repeat(52) { deck.draw() } - - shouldThrow<IllegalStateException> { deck.draw() } - } -}) diff --git a/src/test/kotlin/blackjack/domain/GameTableTest.kt b/src/test/kotlin/blackjack/domain/GameTableTest.kt deleted file mode 100644 index 44d328492..000000000 --- a/src/test/kotlin/blackjack/domain/GameTableTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package blackjack.domain - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class GameTableTest : StringSpec({ - "최초 딜 시 카드를 2장 나누어준다" { - val users = - listOf( - User("홍길동", Cards(emptyList())), - User("홍길덩", Cards(emptyList())), - ) - val initCardReceivedUsers = GameTable.dealInitCard(users, Deck.create()) - - initCardReceivedUsers.forEach { - it.cards.values.size shouldBe 2 - } - } -}) diff --git a/src/test/kotlin/blackjack/domain/RankTest.kt b/src/test/kotlin/blackjack/domain/RankTest.kt deleted file mode 100644 index 350438fd0..000000000 --- a/src/test/kotlin/blackjack/domain/RankTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package blackjack.domain - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.StringSpec -import io.kotest.inspectors.forAll -import io.kotest.matchers.shouldBe - -class RankTest : StringSpec({ - "카드의 랭크가 2~10,J,Q,K,A가 아닌 경우 예외 발생한다" { - listOf("0", "1", "-1", "B", "D").forAll { invalidRank -> - shouldThrow<IllegalArgumentException> { Rank(invalidRank) } - } - } - - "랭크가 숫자이면 점수는 숫자의 값으로 계산된다" { - listOf("2", "3", "4", "5", "6").forAll { number -> - Rank(number).score shouldBe number.toInt() - } - } - - "랭크가 J,Q,K 이면 점수는 10점으로 계산된다" { - listOf("J", "Q", "K").forAll { face -> - Rank(face).score shouldBe 10 - } - } - - "랭크가 에이스이면 기본점수는 11점이다" { - Rank("A").score shouldBe 11 - } -}) diff --git a/src/test/kotlin/blackjack/domain/UserTest.kt b/src/test/kotlin/blackjack/domain/UserTest.kt deleted file mode 100644 index ce7ef2cfe..000000000 --- a/src/test/kotlin/blackjack/domain/UserTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package blackjack.domain - -import blackjack.fixtures.createCard -import io.kotest.core.spec.style.StringSpec -import io.kotest.data.forAll -import io.kotest.data.headers -import io.kotest.data.row -import io.kotest.data.table -import io.kotest.matchers.shouldBe - -class UserTest : StringSpec({ - - "유저는 카드목록의 점수합이 21점 미만일 경우 카드를 더 받을 수 있다" { - table( - headers("ranks"), - row(listOf("2", "3", "4")), - row(listOf("4", "5", "10")), - row(listOf("K", "Q")), - row(listOf("5", "5", "4", "3", "2")), - ).forAll { ranks -> - val cards = Cards(ranks.map { createCard(it) }) - - User("홍길동", cards).canReceiveCard() shouldBe true - } - } - - "유저는 카드목록의 점수합이 21점 이상할 경우 카드를 더 받을 수 없다" { - table( - headers("ranks", "score"), - row(listOf("J", "Q", "K"), 30), - row(listOf("A", "K"), 21), - row(listOf("Q", "10", "A"), 21), - row(listOf("10", "9", "2"), 21), - row(listOf("10", "10", "3"), 23), - ).forAll { ranks, score -> - val cards = Cards(ranks.map { createCard(it) }) - - cards.score shouldBe score - User("홍길동", cards).canReceiveCard() shouldBe false - } - } - - "유저는 카드 2장을 받은 후 점수 합이 21점인 경우 카드를 더 받지 못한다" { - val cards = Cards(emptyList()) - val user = - User("홍길동", cards) - .receiveCard(createCard("A")) - .receiveCard(createCard("K")) - - user.canReceiveCard() shouldBe false - } - - "유저는 카드 2장을 받은 후 점수 합이 21점 미만인 경우 카드를 더 받을 수 있다" { - val cards = Cards(emptyList()) - val user = - User("홍길동", cards) - .receiveCard(createCard("10")) - .receiveCard(createCard("5")) - - user.canReceiveCard() shouldBe true - } -}) diff --git a/src/test/kotlin/blackjack/domain/card/DeckTest.kt b/src/test/kotlin/blackjack/domain/card/DeckTest.kt new file mode 100644 index 000000000..b82dcc0d4 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/card/DeckTest.kt @@ -0,0 +1,30 @@ +package blackjack.domain.card + +import blackjack.domain.card.Rank.ACE +import blackjack.fixtures.createCard +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class DeckTest : BehaviorSpec({ + Given("덱에 카드가 존재하는 경우") { + val deck = Deck(mutableListOf(createCard("A"))) + + When("드로우하면") { + Then("카드를 반환한다") { + deck.draw().rank shouldBe ACE + } + } + } + Given("덱에 카드가 없는 경우") { + val deck = Deck(mutableListOf()) + + When("드로우하면") { + Then("예외 발생한다") { + shouldThrow<IllegalStateException> { + deck.draw() + } + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/CardsTest.kt b/src/test/kotlin/blackjack/domain/card/HandTest.kt similarity index 54% rename from src/test/kotlin/blackjack/domain/CardsTest.kt rename to src/test/kotlin/blackjack/domain/card/HandTest.kt index 280d5844d..e53a937f6 100644 --- a/src/test/kotlin/blackjack/domain/CardsTest.kt +++ b/src/test/kotlin/blackjack/domain/card/HandTest.kt @@ -1,15 +1,15 @@ -package blackjack.domain +package blackjack.domain.card import blackjack.fixtures.createCard -import io.kotest.core.spec.style.StringSpec +import io.kotest.core.spec.style.BehaviorSpec import io.kotest.data.forAll import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table import io.kotest.matchers.shouldBe -class CardsTest : StringSpec({ - "카드목록의 점수합이 21을 초과할 경우 에이스는 1점으로 보정된다" { +class HandTest : BehaviorSpec({ + Given("카드목록의 점수 합이 21점을 초과하는 경우") { table( headers("ranks", "expected"), row(listOf("A", "2", "10"), 13), @@ -18,9 +18,13 @@ class CardsTest : StringSpec({ row(listOf("A", "A", "A"), 13), row(listOf("A", "3", "9"), 13), ).forAll { ranks, expected -> - val cards = Cards(ranks.map { createCard(it) }) + val hand = Hand(ranks.map { createCard(it) }.toMutableList()) - cards.score shouldBe expected + When("점수 계산하면") { + Then("에이스는 1점으로 보정된다") { + hand.score shouldBe expected + } + } } } }) diff --git a/src/test/kotlin/blackjack/domain/card/RankTest.kt b/src/test/kotlin/blackjack/domain/card/RankTest.kt new file mode 100644 index 000000000..0d099ad8f --- /dev/null +++ b/src/test/kotlin/blackjack/domain/card/RankTest.kt @@ -0,0 +1,30 @@ +package blackjack.domain.card + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +class RankTest : StringSpec({ + "유효하지 않은 랭크값을 통해 랭크 생성시 예외 발생한다" { + listOf("0", "1", "-1", "B", "D").forAll { invalidRankValue -> + shouldThrow<IllegalArgumentException> { Rank.from(invalidRankValue) } + } + } + + "랭크가 숫자이면 점수는 숫자의 값이다" { + listOf("2", "3", "4", "5", "6").forAll { number -> + Rank.from(number).score shouldBe number.toInt() + } + } + + "랭크가 J,Q,K 이면 점수는 10점이다" { + listOf("J", "Q", "K").forAll { face -> + Rank.from(face).score shouldBe 10 + } + } + + "랭크가 에이스이면 기본점수는 11점이다" { + Rank.from("A").score shouldBe 11 + } +}) diff --git a/src/test/kotlin/blackjack/domain/game/GameResultTest.kt b/src/test/kotlin/blackjack/domain/game/GameResultTest.kt new file mode 100644 index 000000000..0864293be --- /dev/null +++ b/src/test/kotlin/blackjack/domain/game/GameResultTest.kt @@ -0,0 +1,66 @@ +package blackjack.domain.game + +import blackjack.domain.card.Suit.DIAMOND +import blackjack.domain.card.Suit.HEART +import blackjack.domain.card.Suit.SPADE +import blackjack.domain.player.Dealer +import blackjack.domain.player.Player +import blackjack.fixtures.createCard +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class GameResultTest : BehaviorSpec({ + Given("플레이어가 이긴 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("3", SPADE)) + player1.hit(createCard("4", HEART)) + player2.hit(createCard("5", DIAMOND)) + + When("게임결과를 생성하면") { + val actual = GameResult.from(dealer, listOf(player1, player2)) + + Then("딜러의 패배 카운트가 증가한다") { + actual.dealerGameResult.loseCount shouldBe 2 + } + } + } + + Given("플레이어가 진 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("10", SPADE)) + player1.hit(createCard("4", HEART)) + player2.hit(createCard("5", DIAMOND)) + + When("게임결과를 생성하면") { + val actual = GameResult.from(dealer, listOf(player1, player2)) + + Then("딜러의 승리 카운트가 증가한다") { + actual.dealerGameResult.winCount shouldBe 2 + } + } + } + + Given("플레이어가 딜러와 무승부인 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("10", SPADE)) + player1.hit(createCard("10", HEART)) + player2.hit(createCard("10", DIAMOND)) + + When("게임결과를 생성하면") { + val actual = GameResult.from(dealer, listOf(player1, player2)) + + Then("딜러의 무승부 카운트가 증가한다") { + actual.dealerGameResult.drawCount 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 000000000..7f4b9cb6e --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/DealerTest.kt @@ -0,0 +1,42 @@ +package blackjack.domain.player + +import blackjack.fixtures.createCard +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class DealerTest : BehaviorSpec({ + Given("딜러 히트하는 경우") { + val dealer = Dealer() + + When("히트하면") { + dealer.hit(createCard("J")) + + Then("카드를 추가로 갖는다") { + dealer.hand.cards.count() shouldBe 1 + } + } + } + + Given("딜러 핸드가 17점 이상인 경우") { + val dealer = Dealer() + dealer.hit(createCard("J")) + dealer.hit(createCard("Q")) + + When("히트 가능 여부 질의하면") { + Then("결과 false 이다") { + dealer.canHit() shouldBe false + } + } + } + + Given("딜러 핸드가 17점 미만인 경우") { + val dealer = Dealer() + dealer.hit(createCard("J")) + + When("히트 가능 여부 질의하면") { + Then("결과는 true 이다") { + dealer.canHit() shouldBe true + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/PlayerTest.kt b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt new file mode 100644 index 000000000..6ddb5c533 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt @@ -0,0 +1,140 @@ +package blackjack.domain.player + +import blackjack.domain.card.Hand +import blackjack.domain.card.Suit.DIAMOND +import blackjack.domain.card.Suit.HEART +import blackjack.domain.card.Suit.SPADE +import blackjack.domain.game.MatchResult.DRAW +import blackjack.domain.game.MatchResult.LOSE +import blackjack.domain.game.MatchResult.WIN +import blackjack.fixtures.createCard +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.data.forAll +import io.kotest.data.headers +import io.kotest.data.row +import io.kotest.data.table +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +class PlayerTest : BehaviorSpec({ + Given("플레이어 핸드가 21점 미만인 경우") { + table( + headers("ranks"), + row(listOf("2", "3", "4")), + row(listOf("4", "5", "10")), + row(listOf("K", "Q")), + row(listOf("5", "5", "4", "3", "2")), + ).forAll { ranks -> + val player = Player("홍길동", Hand(ranks.map { createCard(it) }.toMutableList())) + + When("히트 가능 여부 질의하면") { + Then("결과는 true 이다") { + player.canHit() shouldBe true + } + } + } + } + + Given("플레이어 핸드가 21점 이상인 경우") { + table( + headers("ranks"), + row(listOf("J", "Q", "K")), + row(listOf("A", "K")), + row(listOf("Q", "10", "A")), + row(listOf("10", "9", "2")), + row(listOf("10", "10", "3")), + ).forAll { ranks -> + val player = Player("홍길동", Hand(ranks.map { createCard(it) }.toMutableList())) + + When("히트 가능 여부 질의하면") { + Then("결과는 false 이다") { + player.canHit() shouldBe false + } + } + } + } + + Given("플레이어가 딜러보다 점수가 높은 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("3")) + + player1.hit(createCard("4")) + player2.hit(createCard("5")) + + When("게임 결과 비교하면") { + Then("플레이어가 승리한다") { + listOf(player1, player2).forAll { player -> player.matchHand(dealer) shouldBe WIN } + } + } + } + + Given("플레이어가 딜러보다 점수가 낮은 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("10")) + + player1.hit(createCard("4")) + player2.hit(createCard("5")) + + When("게임 결과 비교하면") { + Then("플레이어가 승리한다") { + listOf(player1, player2).forAll { player -> player.matchHand(dealer) shouldBe LOSE } + } + } + } + + Given("플레이어가 딜러보다 점수가 동일한 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("10", SPADE)) + + player1.hit(createCard("10", HEART)) + player2.hit(createCard("10", DIAMOND)) + + When("게임 결과 비교하면") { + Then("플레이어가 승리한다") { + listOf(player1, player2).forAll { player -> player.matchHand(dealer) shouldBe DRAW } + } + } + } + + Given("딜러가 버스트인 경우") { + val dealer = Dealer() + val player1 = Player("유저1") + val player2 = Player("유저2") + + dealer.hit(createCard("K", SPADE)) + dealer.hit(createCard("Q", SPADE)) + dealer.hit(createCard("J", SPADE)) + + player1.hit(createCard("7", HEART)) + player2.hit(createCard("8", DIAMOND)) + + When("게임 결과 비교하면") { + Then("모든 플레이어가 승리한다") { + listOf(player1, player2).forAll { player -> player.matchHand(dealer) shouldBe WIN } + } + } + + When("게임 결과 비교하면 플레이어 핸드가 21점을 초과해도") { + player1.hit(createCard("K", HEART)) + player1.hit(createCard("Q", HEART)) + player1.hit(createCard("J", HEART)) + + player2.hit(createCard("K", DIAMOND)) + player2.hit(createCard("Q", DIAMOND)) + player2.hit(createCard("J", DIAMOND)) + + Then("모든 플레이어가 승리한다") { + listOf(player1, player2).forAll { player -> player.matchHand(dealer) shouldBe WIN } + } + } + } +}) diff --git a/src/test/kotlin/blackjack/fixtures/CardFixture.kt b/src/test/kotlin/blackjack/fixtures/CardFixture.kt index 69494ab58..caf9c481b 100644 --- a/src/test/kotlin/blackjack/fixtures/CardFixture.kt +++ b/src/test/kotlin/blackjack/fixtures/CardFixture.kt @@ -1,13 +1,13 @@ package blackjack.fixtures -import blackjack.domain.Card -import blackjack.domain.Rank -import blackjack.domain.Suit -import blackjack.domain.Suit.SPADE +import blackjack.domain.card.Card +import blackjack.domain.card.Rank +import blackjack.domain.card.Suit +import blackjack.domain.card.Suit.SPADE fun createCard( rank: String, suit: Suit = SPADE, ): Card { - return Card(Rank(rank), suit) + return Card(Rank.from(rank), suit) }