diff --git a/README.md b/README.md index 71f85ac028..946365e5c6 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,26 @@ Player.hit() : 블랙잭에서 카드를 받는 행동은 "히트" 딜러가 - 매직넘버, 매직스트링을 상수로 분리해내기 (특히 view, model) - 어떤 테스트코드가 필요한지 생각하고(뭘 구현해야하지, 뭐가 요구조건인지 리마인드) >> 테스트코드를 작성하고 >> 테스트할수 있도록 리팩토링하기 ``` + +

+ +## 🚀 3단계 - 블랙잭(딜러) + +- 요구사항 분석 +```text +- 딜러 기능을 구현 + - 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 기능을 구현 + - 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다 + - 딜러의 히트/스텐드 전략은 자동이라는 의미임. 테스트코드 작성 필요 + +- 심판기능(Referee) : 점수계산 $& 승패를 판단 + - 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리한다. + - 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이긴다 + +-입출력 기능 + - 게임을 완료한 후 각 플레이어별로 승패를 출력 + +``` + + +walkover - 부전승, 낙승 : 상대방(딜러의 몰수패로 이기는) diff --git a/src/main/kotlin/blackjack/BlackJackGame.kt b/src/main/kotlin/blackjack/BlackJackGame.kt new file mode 100644 index 0000000000..816de9ecbe --- /dev/null +++ b/src/main/kotlin/blackjack/BlackJackGame.kt @@ -0,0 +1,38 @@ +package blackjack + +import blackjack.model.card.pack.Pack +import blackjack.model.player.Participants +import blackjack.model.player.playable.impl.Player +import blackjack.model.player.playblestrategy.PlayingStrategy +import blackjack.model.player.playblestrategy.impl.ConsoleInputStrategy +import blackjack.model.player.playblestrategy.impl.DealerStrategy +import blackjack.view.OutputView + +class BlackJackGame( + private val participants: Participants, + private val pack: Pack, +) { + fun start() { + while (participants.isContinue()) { + playingBlackJackTurn(pack) + } + } + + private fun playingBlackJackTurn(pack: Pack) { + playersTurn(pack) + val dealerScore = participants.dealer.score() + dealerTurn(DealerStrategy(dealerScore), pack) + } + + private fun dealerTurn(playingStrategy: PlayingStrategy, pack: Pack) { + val playing = participants.dealer.playing(playingStrategy, pack) + OutputView.presentDealerAction(playing) + } + + private fun playersTurn(pack: Pack) { + participants.players.values.forEach { player: Player -> + player.playing(ConsoleInputStrategy(player), pack) + OutputView.presentPlayer(player) + } + } +} diff --git a/src/main/kotlin/blackjack/Controller.kt b/src/main/kotlin/blackjack/Controller.kt index 01b613427d..c2c243bdc1 100644 --- a/src/main/kotlin/blackjack/Controller.kt +++ b/src/main/kotlin/blackjack/Controller.kt @@ -1,30 +1,15 @@ package blackjack -import blackjack.model.Participants -import blackjack.model.Player +import blackjack.model.card.pack.impl.ShuffledPack import blackjack.view.InputView import blackjack.view.OutputView fun main() { val participants = InputView.join() - participants.dealing() + participants.dealing(ShuffledPack) OutputView.dealing(participants) OutputView.presentCards(participants) - while (participants.isContinue()) { - playingBlackJack(participants) - } - OutputView.result(participants) -} - -fun playingBlackJack(participants: Participants) { - participants.participants.forEach { - it.hitOrStand() - } -} - -private fun Player.hitOrStand() { - if (InputView.askHit(this)) { - this.hit() - } - OutputView.playerCardPresent(this) + BlackJackGame(participants, ShuffledPack).start() + OutputView.presentScores(participants) + OutputView.presentResult(participants) } diff --git a/src/main/kotlin/blackjack/model/Participants.kt b/src/main/kotlin/blackjack/model/Participants.kt deleted file mode 100644 index 875f0f922f..0000000000 --- a/src/main/kotlin/blackjack/model/Participants.kt +++ /dev/null @@ -1,21 +0,0 @@ -package blackjack.model - -class Participants( - val participants: Set, -) { - fun count(): Int { - return participants.size - } - - fun dealing() { - participants.forEach { it.deal() } - } - - private fun isGameOver(): Boolean { - return participants.none { Referee.isBlackJack(it) } - } - - fun isContinue(): Boolean { - return !isGameOver() - } -} diff --git a/src/main/kotlin/blackjack/model/Player.kt b/src/main/kotlin/blackjack/model/Player.kt deleted file mode 100644 index 4d98a6840d..0000000000 --- a/src/main/kotlin/blackjack/model/Player.kt +++ /dev/null @@ -1,17 +0,0 @@ -package blackjack.model - -import blackjack.model.pack.ShuffledPack - -data class Player( - val name: String, - val cards: Cards = Cards.emptyCards(), -) { - fun deal() { - cards.add(ShuffledPack.pickCard()) - cards.add(ShuffledPack.pickCard()) - } - - fun hit() { - cards.add(ShuffledPack.pickCard()) - } -} diff --git a/src/main/kotlin/blackjack/model/Referee.kt b/src/main/kotlin/blackjack/model/Referee.kt deleted file mode 100644 index 15c30cbe6c..0000000000 --- a/src/main/kotlin/blackjack/model/Referee.kt +++ /dev/null @@ -1,8 +0,0 @@ -package blackjack.model - -object Referee { - private const val BLACK_JACK_SCORE: Int = 21 - fun isBlackJack(it: Player): Boolean { - return it.cards.totalScore() == BLACK_JACK_SCORE - } -} diff --git a/src/main/kotlin/blackjack/model/Suit.kt b/src/main/kotlin/blackjack/model/Suit.kt deleted file mode 100644 index a0e34e2069..0000000000 --- a/src/main/kotlin/blackjack/model/Suit.kt +++ /dev/null @@ -1,12 +0,0 @@ -package blackjack.model - -enum class Suit( - val code: String, - val alias: String, -) { - SPADE("♠", "스페이드"), - HEART("♥", "하트"), - DIAMOND("♦", "다이아"), - CLOVER("♣", "클로버"), - ; -} diff --git a/src/main/kotlin/blackjack/model/blackjack/BlackJackStatus.kt b/src/main/kotlin/blackjack/model/blackjack/BlackJackStatus.kt new file mode 100644 index 0000000000..8eb40c78b8 --- /dev/null +++ b/src/main/kotlin/blackjack/model/blackjack/BlackJackStatus.kt @@ -0,0 +1,6 @@ +package blackjack.model.blackjack + +enum class BlackJackStatus { + ALIVE, + STOP, +} diff --git a/src/main/kotlin/blackjack/model/blackjack/judgment/Judgment.kt b/src/main/kotlin/blackjack/model/blackjack/judgment/Judgment.kt new file mode 100644 index 0000000000..add7ed7b31 --- /dev/null +++ b/src/main/kotlin/blackjack/model/blackjack/judgment/Judgment.kt @@ -0,0 +1,8 @@ +package blackjack.model.blackjack.judgment + +import blackjack.model.player.playable.Playable +import blackjack.model.result.PlayableResult + +interface Judgment { + fun sentence(thisPlayable: Playable, otherPlayable: Playable): PlayableResult +} diff --git a/src/main/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgment.kt b/src/main/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgment.kt new file mode 100644 index 0000000000..5daf703bc7 --- /dev/null +++ b/src/main/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgment.kt @@ -0,0 +1,20 @@ +package blackjack.model.blackjack.judgment.impl + +import blackjack.model.blackjack.judgment.Judgment +import blackjack.model.player.playable.Playable +import blackjack.model.result.PlayableResult + +object BlackJackJudgment : Judgment { + override fun sentence(thisPlayable: Playable, otherPlayable: Playable): PlayableResult { + if (thisPlayable.isBurst() && otherPlayable.isBurst()) { + return PlayableResult.DRAW + } + if (thisPlayable.isBurst() && !otherPlayable.isBurst()) { + return PlayableResult.LOSE + } + if (!thisPlayable.isBurst() && otherPlayable.isBurst()) { + return PlayableResult.WIN + } + return thisPlayable.score() vs otherPlayable.score() + } +} diff --git a/src/main/kotlin/blackjack/model/Card.kt b/src/main/kotlin/blackjack/model/card/Card.kt similarity index 52% rename from src/main/kotlin/blackjack/model/Card.kt rename to src/main/kotlin/blackjack/model/card/Card.kt index 25808b1c81..7d17967bf9 100644 --- a/src/main/kotlin/blackjack/model/Card.kt +++ b/src/main/kotlin/blackjack/model/card/Card.kt @@ -1,20 +1,20 @@ -package blackjack.model +package blackjack.model.card class Card private constructor( val suit: Suit, val cardRank: CardRank, ) { companion object { - private val CARDS: Map, Card> by lazy { + private val CARDS: Map by lazy { Suit.values().flatMap { suit -> CardRank.values().map { rank -> - (suit to rank) to Card(suit, rank) + Trump(suit to rank) to Card(suit, rank) } }.toMap() } fun of(suit: Suit, cardRank: CardRank): Card { - return requireNotNull(this.CARDS[suit to cardRank]) { "입력된 suit=[$suit] , cardRank=[$cardRank] 는 찾을수 없습니다" } + return requireNotNull(CARDS[Trump(suit to cardRank)]) { "입력된 suit=[$suit] , cardRank=[$cardRank] 는 찾을수 없습니다" } } } } diff --git a/src/main/kotlin/blackjack/model/CardRank.kt b/src/main/kotlin/blackjack/model/card/CardRank.kt similarity index 91% rename from src/main/kotlin/blackjack/model/CardRank.kt rename to src/main/kotlin/blackjack/model/card/CardRank.kt index e900081fad..47d49e94e8 100644 --- a/src/main/kotlin/blackjack/model/CardRank.kt +++ b/src/main/kotlin/blackjack/model/card/CardRank.kt @@ -1,4 +1,4 @@ -package blackjack.model +package blackjack.model.card enum class CardRank( val score: Int, diff --git a/src/main/kotlin/blackjack/model/Cards.kt b/src/main/kotlin/blackjack/model/card/Cards.kt similarity index 69% rename from src/main/kotlin/blackjack/model/Cards.kt rename to src/main/kotlin/blackjack/model/card/Cards.kt index f678b87c39..30cf21ddf4 100644 --- a/src/main/kotlin/blackjack/model/Cards.kt +++ b/src/main/kotlin/blackjack/model/card/Cards.kt @@ -1,4 +1,6 @@ -package blackjack.model +package blackjack.model.card + +import blackjack.model.player.BlackjackScore class Cards( cards: List = emptyList(), @@ -10,11 +12,11 @@ class Cards( _cards.add(card) } - fun totalScore(): Int { + fun totalScore(): BlackjackScore { if (this.isContainsAce()) { - return scoreWithAce() + return BlackjackScore(scoreWithAce()) } - return simpleSumOfScore() + return BlackjackScore(simpleSumOfScore()) } private fun simpleSumOfScore(): Int { @@ -28,10 +30,10 @@ class Cards( private fun scoreWithAce(): Int { val simpleSum = simpleSumOfScore() - if (simpleSum + ALTERNATIVE_ACE_SCORE_DIFF > HIGHEST_SCORE) { + if (simpleSum + ACE_BONUS_SCORE > HIGHEST_SCORE) { return simpleSum } - return simpleSum + ALTERNATIVE_ACE_SCORE_DIFF + return simpleSum + ACE_BONUS_SCORE } fun count(): Int { @@ -40,7 +42,7 @@ class Cards( companion object { private const val HIGHEST_SCORE: Int = 21 - private const val ALTERNATIVE_ACE_SCORE_DIFF: Int = 11 - 1 + private const val ACE_BONUS_SCORE: Int = 10 fun emptyCards(): Cards { return Cards(mutableListOf()) diff --git a/src/main/kotlin/blackjack/model/card/Suit.kt b/src/main/kotlin/blackjack/model/card/Suit.kt new file mode 100644 index 0000000000..dd83ffda04 --- /dev/null +++ b/src/main/kotlin/blackjack/model/card/Suit.kt @@ -0,0 +1,11 @@ +package blackjack.model.card + +enum class Suit( + val alias: String, +) { + SPADE("스페이드"), + HEART("하트"), + DIAMOND("다이아"), + CLOVER("클로버"), + ; +} diff --git a/src/main/kotlin/blackjack/model/card/Trump.kt b/src/main/kotlin/blackjack/model/card/Trump.kt new file mode 100644 index 0000000000..a152ad4bc2 --- /dev/null +++ b/src/main/kotlin/blackjack/model/card/Trump.kt @@ -0,0 +1,6 @@ +package blackjack.model.card + +@JvmInline +value class Trump( + val trump: Pair, +) diff --git a/src/main/kotlin/blackjack/model/card/pack/Pack.kt b/src/main/kotlin/blackjack/model/card/pack/Pack.kt new file mode 100644 index 0000000000..beb091ae91 --- /dev/null +++ b/src/main/kotlin/blackjack/model/card/pack/Pack.kt @@ -0,0 +1,7 @@ +package blackjack.model.card.pack + +import blackjack.model.card.Card + +interface Pack { + fun pickCard(): Card +} diff --git a/src/main/kotlin/blackjack/model/pack/ShuffledPack.kt b/src/main/kotlin/blackjack/model/card/pack/impl/ShuffledPack.kt similarity index 68% rename from src/main/kotlin/blackjack/model/pack/ShuffledPack.kt rename to src/main/kotlin/blackjack/model/card/pack/impl/ShuffledPack.kt index 897abc5a71..d24f0f1610 100644 --- a/src/main/kotlin/blackjack/model/pack/ShuffledPack.kt +++ b/src/main/kotlin/blackjack/model/card/pack/impl/ShuffledPack.kt @@ -1,8 +1,9 @@ -package blackjack.model.pack +package blackjack.model.card.pack.impl -import blackjack.model.Card -import blackjack.model.CardRank -import blackjack.model.Suit +import blackjack.model.card.Card +import blackjack.model.card.CardRank +import blackjack.model.card.Suit +import blackjack.model.card.pack.Pack import kotlin.random.Random object ShuffledPack : Pack { @@ -21,6 +22,6 @@ object ShuffledPack : Pack { } private fun randomInRange(range: Int): Int { - return this.random.nextInt(0, Int.MAX_VALUE) % range + return random.nextInt(0, Int.MAX_VALUE) % range } } diff --git a/src/main/kotlin/blackjack/model/pack/Pack.kt b/src/main/kotlin/blackjack/model/pack/Pack.kt deleted file mode 100644 index da8925a27a..0000000000 --- a/src/main/kotlin/blackjack/model/pack/Pack.kt +++ /dev/null @@ -1,7 +0,0 @@ -package blackjack.model.pack - -import blackjack.model.Card - -interface Pack { - fun pickCard(): Card -} diff --git a/src/main/kotlin/blackjack/model/player/BlackjackScore.kt b/src/main/kotlin/blackjack/model/player/BlackjackScore.kt new file mode 100644 index 0000000000..0bf9ff71e2 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/BlackjackScore.kt @@ -0,0 +1,30 @@ +package blackjack.model.player + +import blackjack.model.result.PlayableResult + +@JvmInline +value class BlackjackScore( + val value: Int +) { + infix fun vs(other: BlackjackScore): PlayableResult { + if (this.value == other.value) { + return PlayableResult.DRAW + } + if (this.value > other.value) { + return PlayableResult.WIN + } + return PlayableResult.LOSE + } + + fun isBurst(): Boolean { + return this.value > BLACK_JACK_SCORE + } + + operator fun compareTo(other: BlackjackScore): Int { + return this.value.compareTo(other.value) + } + + companion object { + private const val BLACK_JACK_SCORE: Int = 21 + } +} diff --git a/src/main/kotlin/blackjack/model/player/Participants.kt b/src/main/kotlin/blackjack/model/player/Participants.kt new file mode 100644 index 0000000000..c0e15db1b1 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/Participants.kt @@ -0,0 +1,28 @@ +package blackjack.model.player + +import blackjack.model.card.pack.Pack +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.result.DealerResult + +class Participants( + val players: Players, + val dealer: Dealer, +) { + + fun dealing(pack: Pack) { + players.dealing(pack) + dealer.dealing(pack) + } + + fun isContinue(): Boolean { + return players.hasAnyAlivePlayer() && (!dealer.isBurst()) + } + + fun count(): Int { + return players.count() + 1 + } + + fun dealerResult(): DealerResult { + return this.dealer.dealerResult(this.players) + } +} diff --git a/src/main/kotlin/blackjack/model/player/PlayableReaction.kt b/src/main/kotlin/blackjack/model/player/PlayableReaction.kt new file mode 100644 index 0000000000..07f03c8333 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/PlayableReaction.kt @@ -0,0 +1,6 @@ +package blackjack.model.player + +enum class PlayableReaction { + HIT, + STAND +} diff --git a/src/main/kotlin/blackjack/model/player/Players.kt b/src/main/kotlin/blackjack/model/player/Players.kt new file mode 100644 index 0000000000..3448af29d4 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/Players.kt @@ -0,0 +1,34 @@ +package blackjack.model.player + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.card.pack.Pack +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player +import blackjack.model.result.PlayableResult + +class Players( + val values: List, +) { + init { + require(values.map { it.name }.distinct().size == values.size) { "Player 들의 이름은 중복이 허용되지 않습니다" } + } + + constructor(vararg players: Player) : this(players.toList()) + + fun dealing(pack: Pack) { + values.forEach { it.dealing(pack) } + } + + fun hasAnyAlivePlayer(): Boolean { + val burstPlayers: List = values.map { it.status() == BlackJackStatus.ALIVE }.toList() + return burstPlayers.contains(true) + } + + fun count(): Int { + return values.size + } + + fun results(dealer: Dealer): List> { + return this.values.map { player -> player to player.result(dealer) } + } +} diff --git a/src/main/kotlin/blackjack/model/player/playable/Playable.kt b/src/main/kotlin/blackjack/model/player/playable/Playable.kt new file mode 100644 index 0000000000..1de3a8b8fd --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playable/Playable.kt @@ -0,0 +1,17 @@ +package blackjack.model.player.playable + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.card.pack.Pack +import blackjack.model.player.BlackjackScore +import blackjack.model.player.PlayableReaction +import blackjack.model.player.playblestrategy.PlayingStrategy +import blackjack.model.result.PlayableResult + +interface Playable { + fun score(): BlackjackScore + fun dealing(pack: Pack) + fun playing(playingStrategy: PlayingStrategy, pack: Pack): PlayableReaction + fun result(playable: Playable): PlayableResult + fun isBurst(): Boolean + fun status(): BlackJackStatus +} diff --git a/src/main/kotlin/blackjack/model/player/playable/impl/Dealer.kt b/src/main/kotlin/blackjack/model/player/playable/impl/Dealer.kt new file mode 100644 index 0000000000..e1bcb0ba15 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playable/impl/Dealer.kt @@ -0,0 +1,63 @@ +package blackjack.model.player.playable.impl + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.blackjack.judgment.impl.BlackJackJudgment +import blackjack.model.card.Cards +import blackjack.model.card.pack.Pack +import blackjack.model.player.BlackjackScore +import blackjack.model.player.PlayableReaction +import blackjack.model.player.Players +import blackjack.model.player.playable.Playable +import blackjack.model.player.playblestrategy.PlayingStrategy +import blackjack.model.result.DealerResult +import blackjack.model.result.PlayableResult + +class Dealer( + val cards: Cards = Cards(), +) : Playable { + + override fun score(): BlackjackScore { + return cards.totalScore() + } + + override fun dealing(pack: Pack) { + this.cards.add(pack.pickCard()) + this.cards.add(pack.pickCard()) + } + + fun countOfCards(): Int { + return cards.count() + } + + override fun playing(playingStrategy: PlayingStrategy, pack: Pack): PlayableReaction { + if (playingStrategy.isHit()) { + cards.add(pack.pickCard()) + return PlayableReaction.HIT + } + return PlayableReaction.STAND + } + + override fun result(playable: Playable): PlayableResult { + return BlackJackJudgment.sentence(this, playable) + } + + override fun status(): BlackJackStatus { + if (this.score().isBurst()) { + return BlackJackStatus.STOP + } + return BlackJackStatus.ALIVE + } + + override fun isBurst(): Boolean { + return this.score().isBurst() + } + + fun dealerResult(players: Players): DealerResult { + val results = players.values.map { player -> this.result(player) } + return DealerResult( + winningCount = results.count { it == PlayableResult.WIN }, + drawingCount = results.count { it == PlayableResult.DRAW }, + losingCount = results.count { it == PlayableResult.LOSE }, + ) + } +} diff --git a/src/main/kotlin/blackjack/model/player/playable/impl/Player.kt b/src/main/kotlin/blackjack/model/player/playable/impl/Player.kt new file mode 100644 index 0000000000..1acf47a52b --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playable/impl/Player.kt @@ -0,0 +1,59 @@ +package blackjack.model.player.playable.impl + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.blackjack.judgment.impl.BlackJackJudgment +import blackjack.model.card.Cards +import blackjack.model.card.pack.Pack +import blackjack.model.player.BlackjackScore +import blackjack.model.player.PlayableReaction +import blackjack.model.player.playable.Playable +import blackjack.model.player.playblestrategy.PlayingStrategy +import blackjack.model.result.PlayableResult + +class Player( + val name: String, + val cards: Cards = Cards.emptyCards(), + var status: BlackJackStatus, +) : Playable { + + override fun score(): BlackjackScore { + return this.cards.totalScore() + } + + override fun dealing(pack: Pack) { + cards.add(pack.pickCard()) + cards.add(pack.pickCard()) + } + + override fun playing(playingStrategy: PlayingStrategy, pack: Pack): PlayableReaction { + if (playingStrategy.isHit()) { + this.hit(pack) + return PlayableReaction.HIT + } + this.status = BlackJackStatus.STOP + return PlayableReaction.STAND + } + + override fun result(playable: Playable): PlayableResult { + return BlackJackJudgment.sentence(this, playable) + } + + override fun isBurst(): Boolean { + if (this.score().isBurst()) { + this.status = BlackJackStatus.STOP + return true + } + return false + } + + fun hit(pack: Pack) { + cards.add(pack.pickCard()) + } + + override fun status(): BlackJackStatus { + if (this.isBurst()) { + return BlackJackStatus.STOP + } + return this.status + } +} diff --git a/src/main/kotlin/blackjack/model/player/playblestrategy/PlayingStrategy.kt b/src/main/kotlin/blackjack/model/player/playblestrategy/PlayingStrategy.kt new file mode 100644 index 0000000000..4e18ce86b4 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playblestrategy/PlayingStrategy.kt @@ -0,0 +1,5 @@ +package blackjack.model.player.playblestrategy + +interface PlayingStrategy { + fun isHit(): Boolean +} diff --git a/src/main/kotlin/blackjack/model/player/playblestrategy/impl/ConsoleInputStrategy.kt b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/ConsoleInputStrategy.kt new file mode 100644 index 0000000000..0499e43933 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/ConsoleInputStrategy.kt @@ -0,0 +1,22 @@ +package blackjack.model.player.playblestrategy.impl + +import blackjack.model.player.playable.impl.Player +import blackjack.model.player.playblestrategy.PlayingStrategy + +class ConsoleInputStrategy( + val player: Player, +) : PlayingStrategy { + + override fun isHit(): Boolean { + println("${player.name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + return this.isHitInput() + } + + private fun isHitInput(): Boolean { + return (readlnOrNull() ?: "") == HIT_COMMAND + } + + companion object { + private const val HIT_COMMAND: String = "y" + } +} diff --git a/src/main/kotlin/blackjack/model/player/playblestrategy/impl/DealerStrategy.kt b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/DealerStrategy.kt new file mode 100644 index 0000000000..19a2e22435 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/DealerStrategy.kt @@ -0,0 +1,17 @@ +package blackjack.model.player.playblestrategy.impl + +import blackjack.model.player.BlackjackScore +import blackjack.model.player.playblestrategy.PlayingStrategy + +data class DealerStrategy( + private val currentScore: BlackjackScore, +) : PlayingStrategy { + + override fun isHit(): Boolean { + return currentScore <= DEALER_PICK_THRESHOLD + } + + companion object { + private val DEALER_PICK_THRESHOLD = BlackjackScore(16) + } +} diff --git a/src/main/kotlin/blackjack/model/player/playblestrategy/impl/HitStrategy.kt b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/HitStrategy.kt new file mode 100644 index 0000000000..9458e9ee52 --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/HitStrategy.kt @@ -0,0 +1,9 @@ +package blackjack.model.player.playblestrategy.impl + +import blackjack.model.player.playblestrategy.PlayingStrategy + +object HitStrategy : PlayingStrategy { + override fun isHit(): Boolean { + return true + } +} diff --git a/src/main/kotlin/blackjack/model/player/playblestrategy/impl/StandStrategy.kt b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/StandStrategy.kt new file mode 100644 index 0000000000..d2a7dbc43a --- /dev/null +++ b/src/main/kotlin/blackjack/model/player/playblestrategy/impl/StandStrategy.kt @@ -0,0 +1,9 @@ +package blackjack.model.player.playblestrategy.impl + +import blackjack.model.player.playblestrategy.PlayingStrategy + +object StandStrategy : PlayingStrategy { + override fun isHit(): Boolean { + return false + } +} diff --git a/src/main/kotlin/blackjack/model/result/DealerResult.kt b/src/main/kotlin/blackjack/model/result/DealerResult.kt new file mode 100644 index 0000000000..db8f13cd0f --- /dev/null +++ b/src/main/kotlin/blackjack/model/result/DealerResult.kt @@ -0,0 +1,7 @@ +package blackjack.model.result + +data class DealerResult( + val winningCount: Int, + val drawingCount: Int, + val losingCount: Int, +) diff --git a/src/main/kotlin/blackjack/model/result/PlayableResult.kt b/src/main/kotlin/blackjack/model/result/PlayableResult.kt new file mode 100644 index 0000000000..e2e94cda8f --- /dev/null +++ b/src/main/kotlin/blackjack/model/result/PlayableResult.kt @@ -0,0 +1,7 @@ +package blackjack.model.result + +enum class PlayableResult { + WIN, + LOSE, + DRAW, +} diff --git a/src/main/kotlin/blackjack/view/Console.kt b/src/main/kotlin/blackjack/view/Console.kt deleted file mode 100644 index a57cba1bfe..0000000000 --- a/src/main/kotlin/blackjack/view/Console.kt +++ /dev/null @@ -1,15 +0,0 @@ -package blackjack.view - -import blackjack.model.Cards -import blackjack.model.Player - -object Console { - - fun Cards.present(): String { - return cards.joinToString(separator = ", ") { "${it.cardRank.alias}${it.suit.alias}" } - } - - fun Player.present(): String { - return "${this.name}카드 : ${this.cards.present()}" - } -} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt index 08fd3a1cd6..bec3904164 100644 --- a/src/main/kotlin/blackjack/view/InputView.kt +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -1,19 +1,25 @@ package blackjack.view -import blackjack.model.Participants -import blackjack.model.Player +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.player.Participants +import blackjack.model.player.Players +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player object InputView { private const val PLAYER_NAMES_DELIMITER: String = "," - private const val HIT_COMMAND: String = "y" + const val PARTICIPANTS_PRESENT_SEPARATOR: String = ", " private fun joinPlayers(input: String): Participants { return Participants( - input.split(PLAYER_NAMES_DELIMITER) - .asSequence() - .map { Player(it) } - .toSet() + Players( + input.split(PLAYER_NAMES_DELIMITER) + .asSequence() + .map { Player(name = it, status = BlackJackStatus.ALIVE) } + .toList() + ), + Dealer() ) } @@ -21,13 +27,4 @@ object InputView { println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") return joinPlayers(readlnOrNull() ?: "") } - - private fun isHitInput(): Boolean { - return (readlnOrNull() ?: "") == HIT_COMMAND - } - - fun askHit(it: Player): Boolean { - println("${it.name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") - return this.isHitInput() - } } diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt index 24527e6393..2adba6969c 100644 --- a/src/main/kotlin/blackjack/view/OutputView.kt +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -1,41 +1,93 @@ package blackjack.view -import blackjack.model.Participants -import blackjack.model.Player -import blackjack.view.Console.present +import blackjack.model.card.Cards +import blackjack.model.player.Participants +import blackjack.model.player.PlayableReaction +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player +import blackjack.model.result.DealerResult +import blackjack.model.result.PlayableResult object OutputView { fun presentCards(participants: Participants) { - println(participants.present()) + println(participants.presentDealer()) + println(participants.presentPlayers()) } - fun result(participants: Participants) { + fun presentScores(participants: Participants) { presentCardsWitScore(participants) } private fun presentCardsWitScore(participants: Participants) { - println() - println(participants.presentWithScore()) + print(System.lineSeparator()) + println(participants.presentDealerWithScore()) + println(participants.presentPlayerWithScore()) } fun dealing(participants: Participants) { - println("${participants.names()} 에게 2 장씩 나누었습니다.") + println("딜러와 ${participants.names()} 에게 2 장씩 나누었습니다.") } - fun playerCardPresent(it: Player) { - println(it.present()) + fun presentResult(participants: Participants) { + println("## 최종 승패") + presentDealerResult(participants.dealerResult()) + presentPlayersResult(participants.players.results(participants.dealer)) + } + + private fun presentPlayersResult(playersResult: List>) { + playersResult + .forEach { presentPlayerResult(it.first, it.second) } + } + + private fun presentPlayerResult(player: Player, result: PlayableResult) { + println("${player.name}: ${result.name}") + } + + fun presentPlayer(player: Player) { + println(player.presentPlayers()) + } + + private fun presentDealerResult(dealerResult: DealerResult) { + println("딜러 ${dealerResult.winningCount}승 ${dealerResult.drawingCount}무 ${dealerResult.losingCount}패") + } + + fun presentDealerAction(playableReaction: PlayableReaction) { + when (playableReaction) { + PlayableReaction.HIT -> println("딜러는 16이하라 한장의 카드를 더 받았습니다.") + PlayableReaction.STAND -> println("딜러는 17이상이라 카드를 받지 않았습니다.") + } } } -private fun Participants.present(): String { - return this.participants.joinToString(separator = "\n") { it.present() } +private fun Participants.presentPlayers(): String { + return this.players.values.joinToString(separator = "\n") { it.presentPlayers() } } -private fun Participants.presentWithScore(): String { - return this.participants.joinToString(separator = "\n") { "${it.present()} - 결과: ${it.cards.totalScore()}" } +private fun Participants.presentDealer(): String { + return this.dealer.presentDealers() +} + +private fun Participants.presentDealerWithScore(): String { + return "${this.dealer.presentDealers()} - 결과: ${this.dealer.score().value}" +} + +private fun Participants.presentPlayerWithScore(): String { + return this.players.values.joinToString(separator = "\n") { "${it.presentPlayers()} - 결과: ${it.cards.totalScore().value}" } } private fun Participants.names(): String { - return participants.joinToString(separator = InputView.PARTICIPANTS_PRESENT_SEPARATOR) { it.name } + return players.values.joinToString(separator = InputView.PARTICIPANTS_PRESENT_SEPARATOR) { it.name } +} + +fun Cards.presentPlayers(): String { + return cards.joinToString(separator = ", ") { "${it.cardRank.alias}${it.suit.alias}" } +} + +fun Player.presentPlayers(): String { + return "${this.name}카드 : ${this.cards.presentPlayers()}" +} + +fun Dealer.presentDealers(): String { + return "딜러 카드 : ${this.cards.presentPlayers()}" } diff --git a/src/test/kotlin/blackjack/model/CardsTest.kt b/src/test/kotlin/blackjack/model/CardsTest.kt deleted file mode 100644 index 0bba60f724..0000000000 --- a/src/test/kotlin/blackjack/model/CardsTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package blackjack.model - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class CardsTest : StringSpec({ - - "Cards 의 모든 카드의 점수의 합을 반환해야 한다" { - val actual = Cards( - listOf( - Card.of(Suit.CLOVER, CardRank.THREE), - Card.of(Suit.DIAMOND, CardRank.FIVE), - Card.of(Suit.HEART, CardRank.EIGHT), - Card.of(Suit.HEART, CardRank.KING) - ) - ).totalScore() - actual shouldBe (3 + 5 + 8 + 10) - } - - "합계점수가 21를 초과 하는 경우, ACE 가 1로 인식되어야 한다" { - val actual = Cards( - listOf( - Card.of(Suit.HEART, CardRank.ACE), - Card.of(Suit.DIAMOND, CardRank.FIVE), - Card.of(Suit.HEART, CardRank.EIGHT), - Card.of(Suit.HEART, CardRank.KING) - ) - ).totalScore() - actual shouldBe (1 + 5 + 8 + 10) - } - - "합계점수가 21를 이하인 경우, ACE 가 11로 인식되어야 한다" { - val actual = Cards( - listOf( - Card.of(Suit.HEART, CardRank.ACE), - Card.of(Suit.HEART, CardRank.THREE), - Card.of(Suit.DIAMOND, CardRank.FIVE), - ) - ).totalScore() - actual shouldBe (11 + 3 + 5) - } -}) diff --git a/src/test/kotlin/blackjack/model/PlayerTest.kt b/src/test/kotlin/blackjack/model/PlayerTest.kt deleted file mode 100644 index 41efb3eb5f..0000000000 --- a/src/test/kotlin/blackjack/model/PlayerTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package blackjack.model - -import io.kotest.assertions.throwables.shouldNotThrow -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class PlayerTest : StringSpec({ - "플레이어는 dealing 시 2장의 카드를 받을 수 있다" { - shouldNotThrow { - val player = Player("구글") - player.deal() - player.cards.count() shouldBe 2 - } - } - - "플레이어는 hit 시 1장의 카드를 받을 수 있다" { - shouldNotThrow { - val player = Player("애플") - player.hit() - player.cards.count() shouldBe 1 - } - } -}) diff --git a/src/test/kotlin/blackjack/model/PlayersTest.kt b/src/test/kotlin/blackjack/model/PlayersTest.kt new file mode 100644 index 0000000000..127ae76dd0 --- /dev/null +++ b/src/test/kotlin/blackjack/model/PlayersTest.kt @@ -0,0 +1,23 @@ +package blackjack.model + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.player.Players +import blackjack.model.player.playable.impl.Player +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec + +class PlayersTest : StringSpec({ + + "플레이어들의 이름은 중복을 허용하지 않는다. 중복시 throw IllegalArgumentException" { + shouldThrow { + Players(Player("hana", status = BlackJackStatus.ALIVE), Player("hana", status = BlackJackStatus.ALIVE)) + } + } + + "플레이어들의 이름이 중복되지 않으면 정상 동작 해야한다" { + shouldNotThrow { + Players(Player("hana", status = BlackJackStatus.ALIVE), Player("numa", status = BlackJackStatus.ALIVE)) + } + } +}) diff --git a/src/test/kotlin/blackjack/model/RefereeTest.kt b/src/test/kotlin/blackjack/model/RefereeTest.kt deleted file mode 100644 index 2da25b0950..0000000000 --- a/src/test/kotlin/blackjack/model/RefereeTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package blackjack.model - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class RefereeTest : StringSpec({ - - "ACE+ACE+KING 이 들어온 경우 ACE 를 1로 인식해, 블랙잭이 아니어야만 한다" { - val player = Player( - "kim", - Cards( - listOf( - Card.of(Suit.HEART, CardRank.ACE), - Card.of(Suit.DIAMOND, CardRank.ACE), - Card.of(Suit.CLOVER, CardRank.KING) - ) - ) - ) - Referee.isBlackJack(player) shouldBe false - } - - "ACE+KING 이 들어온 경우 ACE 를 11로 인식해, 블랙잭으로 계산 되어야 한다" { - val player = Player( - "kim", - Cards( - listOf( - Card.of(Suit.DIAMOND, CardRank.ACE), - Card.of(Suit.CLOVER, CardRank.KING) - ) - ) - ) - Referee.isBlackJack(player) shouldBe true - } -}) diff --git a/src/test/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgmentTest.kt b/src/test/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgmentTest.kt new file mode 100644 index 0000000000..7c165e00ad --- /dev/null +++ b/src/test/kotlin/blackjack/model/blackjack/judgment/impl/BlackJackJudgmentTest.kt @@ -0,0 +1,52 @@ +package blackjack.model.blackjack.judgment.impl + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.card.CardFixture +import blackjack.model.card.CardFixture.makeCards +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player +import blackjack.model.result.PlayableResult +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class BlackJackJudgmentTest : StringSpec({ + + "플레이어와 딜러 모두 burst가 아니라면 점수가 높은 쪽이 승리해야 한다" { + val player = Player("pi", makeCards(CardFixture.two), status = BlackJackStatus.ALIVE) + val dealer = Dealer(makeCards(CardFixture.three)) + + player.result(dealer) shouldBe PlayableResult.LOSE + dealer.result(player) shouldBe PlayableResult.WIN + } + "플레이어와 딜러 모두 burst 라면 DRAW 이어야한다" { + val player = Player("pi", makeCards(CardFixture.jack, CardFixture.queen, CardFixture.five), status = BlackJackStatus.ALIVE) + val dealer = Dealer(makeCards(CardFixture.three, CardFixture.ten, CardFixture.nine)) + + player.result(dealer) shouldBe PlayableResult.DRAW + dealer.result(player) shouldBe PlayableResult.DRAW + } + + "플레이어가 burst 이고, 딜러는 burst 가 아니라면 딜러가 승리해야 한다" { + val player = Player("pi", makeCards(CardFixture.jack, CardFixture.queen, CardFixture.five), status = BlackJackStatus.ALIVE) + val dealer = Dealer(makeCards(CardFixture.three, CardFixture.ten, CardFixture.four)) + + player.result(dealer) shouldBe PlayableResult.LOSE + dealer.result(player) shouldBe PlayableResult.WIN + } + + "딜러가 burst 이고, 플레이어는 burst 가 아니라면 플레이어가 승리해야 한다" { + val player = Player("pi", makeCards(CardFixture.jack, CardFixture.two, CardFixture.five), status = BlackJackStatus.ALIVE) + val dealer = Dealer(makeCards(CardFixture.three, CardFixture.ten, CardFixture.nine)) + + player.result(dealer) shouldBe PlayableResult.WIN + dealer.result(player) shouldBe PlayableResult.LOSE + } + + "플레이어와 딜러 모두 BlackJack(21점) 이라면 DRAW 이어야한다" { + val player = Player("pi", makeCards(CardFixture.jack, CardFixture.queen, CardFixture.ace2), status = BlackJackStatus.ALIVE) + val dealer = Dealer(makeCards(CardFixture.three, CardFixture.ten, CardFixture.eight)) + + player.result(dealer) shouldBe PlayableResult.DRAW + dealer.result(player) shouldBe PlayableResult.DRAW + } +}) diff --git a/src/test/kotlin/blackjack/model/card/CardFixture.kt b/src/test/kotlin/blackjack/model/card/CardFixture.kt new file mode 100644 index 0000000000..29c802fc55 --- /dev/null +++ b/src/test/kotlin/blackjack/model/card/CardFixture.kt @@ -0,0 +1,22 @@ +package blackjack.model.card + +object CardFixture { + fun makeCards(vararg cardArray: Card): Cards { + return Cards(cardArray.toList()) + } + + val ace1 = Card.of(Suit.HEART, CardRank.ACE) + val ace2 = Card.of(Suit.DIAMOND, CardRank.ACE) + val two = Card.of(Suit.DIAMOND, CardRank.TWO) + val three = Card.of(Suit.DIAMOND, CardRank.THREE) + val four = Card.of(Suit.DIAMOND, CardRank.FOUR) + val five = Card.of(Suit.DIAMOND, CardRank.FIVE) + val six = Card.of(Suit.DIAMOND, CardRank.SIX) + val seven = Card.of(Suit.DIAMOND, CardRank.SEVEN) + val eight = Card.of(Suit.DIAMOND, CardRank.EIGHT) + val nine = Card.of(Suit.DIAMOND, CardRank.NINE) + val ten = Card.of(Suit.DIAMOND, CardRank.TEN) + val jack = Card.of(Suit.CLOVER, CardRank.JACK) + val queen = Card.of(Suit.CLOVER, CardRank.QUEEN) + val king = Card.of(Suit.CLOVER, CardRank.KING) +} diff --git a/src/test/kotlin/blackjack/model/card/CardsTest.kt b/src/test/kotlin/blackjack/model/card/CardsTest.kt new file mode 100644 index 0000000000..392415f1db --- /dev/null +++ b/src/test/kotlin/blackjack/model/card/CardsTest.kt @@ -0,0 +1,91 @@ +package blackjack.model.card + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.player.BlackjackScore +import blackjack.model.player.playable.impl.Player +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe + +class CardsTest : StringSpec({ + + "Cards 의 모든 카드의 점수의 합을 반환해야 한다" { + val actual = Cards( + listOf( + Card.of(Suit.CLOVER, CardRank.THREE), + Card.of(Suit.DIAMOND, CardRank.FIVE), + Card.of(Suit.HEART, CardRank.EIGHT), + Card.of(Suit.HEART, CardRank.KING) + ) + ).totalScore() + actual shouldBe BlackjackScore(3 + 5 + 8 + 10) + } + + "합계점수가 21를 초과 하는 경우, ACE 가 1로 인식되어야 한다" { + val actual = Cards( + listOf( + Card.of(Suit.HEART, CardRank.ACE), + Card.of(Suit.DIAMOND, CardRank.FIVE), + Card.of(Suit.HEART, CardRank.EIGHT), + Card.of(Suit.HEART, CardRank.KING) + ) + ).totalScore() + actual shouldBe BlackjackScore(1 + 5 + 8 + 10) + } + + "합계점수가 21를 이하인 경우, ACE 가 11로 인식되어야 한다" { + val actual = Cards( + listOf( + Card.of(Suit.HEART, CardRank.ACE), + Card.of(Suit.HEART, CardRank.THREE), + Card.of(Suit.DIAMOND, CardRank.FIVE), + ) + ).totalScore() + actual shouldBe BlackjackScore(11 + 3 + 5) + } + + "ACE+ACE+KING 이 들어온 경우 ACE 를 1로 인식해, 스코어가 12 이어야 만 한다" { + val player = Player( + "kim", + CardFixture.makeCards(CardFixture.ace1, CardFixture.ace2, CardFixture.king), + status = BlackJackStatus.ALIVE + ) + player.status() shouldBe BlackJackStatus.ALIVE + player.isBurst() shouldBe false + player.score() shouldBe BlackjackScore(12) + } + + "ACE+KING 이 들어온 경우 ACE 를 11로 인식해, 블랙잭 점수인 21 로 계산 되어야 한다" { + val player = Player( + "kong", + CardFixture.makeCards(CardFixture.ace1, CardFixture.king), + status = BlackJackStatus.ALIVE + ) + player.status() shouldBe BlackJackStatus.ALIVE + player.isBurst() shouldBe false + player.score() shouldBe BlackjackScore(21) + } + + "카드가 여러장 들어있는 상황에서도 합계 점수를 정확히 구해낼 수 있어야한다" { + val player = Player( + "kong", + CardFixture.makeCards( + CardFixture.ace1, + CardFixture.two, + CardFixture.three, + CardFixture.four, + CardFixture.five, + CardFixture.six, + CardFixture.seven, + CardFixture.eight, + CardFixture.nine, + CardFixture.ten, + CardFixture.king, + CardFixture.jack, + CardFixture.queen + ), + status = BlackJackStatus.ALIVE + ) + player.score() shouldBeIn listOf(BlackjackScore(85), BlackjackScore(95)) + } +}) diff --git a/src/test/kotlin/blackjack/model/pack/ShuffledPackTest.kt b/src/test/kotlin/blackjack/model/pack/impl/ShuffledPackTest.kt similarity index 80% rename from src/test/kotlin/blackjack/model/pack/ShuffledPackTest.kt rename to src/test/kotlin/blackjack/model/pack/impl/ShuffledPackTest.kt index 69143f7624..890dd308a2 100644 --- a/src/test/kotlin/blackjack/model/pack/ShuffledPackTest.kt +++ b/src/test/kotlin/blackjack/model/pack/impl/ShuffledPackTest.kt @@ -1,5 +1,6 @@ -package blackjack.model.pack +package blackjack.model.pack.impl +import blackjack.model.card.pack.impl.ShuffledPack import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.core.spec.style.StringSpec diff --git a/src/test/kotlin/blackjack/model/playable/BlackjackScoreTest.kt b/src/test/kotlin/blackjack/model/playable/BlackjackScoreTest.kt new file mode 100644 index 0000000000..a9d3d002f3 --- /dev/null +++ b/src/test/kotlin/blackjack/model/playable/BlackjackScoreTest.kt @@ -0,0 +1,20 @@ +package blackjack.model.playable + +import blackjack.model.player.BlackjackScore +import blackjack.model.result.PlayableResult +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class BlackjackScoreTest : StringSpec({ + "이겨야한다" { + BlackjackScore(4) vs BlackjackScore(2) shouldBe PlayableResult.WIN + } + + "져야한다" { + BlackjackScore(1) vs BlackjackScore(2) shouldBe PlayableResult.LOSE + } + + "비겨야한다" { + BlackjackScore(7) vs BlackjackScore(7) shouldBe PlayableResult.DRAW + } +}) diff --git a/src/test/kotlin/blackjack/model/playable/impl/DealerTest.kt b/src/test/kotlin/blackjack/model/playable/impl/DealerTest.kt new file mode 100644 index 0000000000..b77c9361b9 --- /dev/null +++ b/src/test/kotlin/blackjack/model/playable/impl/DealerTest.kt @@ -0,0 +1,140 @@ +package blackjack.model.playable.impl + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.card.CardFixture +import blackjack.model.card.pack.impl.ShuffledPack +import blackjack.model.player.BlackjackScore +import blackjack.model.player.Players +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player +import blackjack.model.player.playblestrategy.impl.DealerStrategy +import blackjack.model.result.PlayableResult +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe + +class DealerTest : StringSpec({ + + "딜러는 처음에 받은 2장의 카드 점수 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 한다" { + val dealer = Dealer( + CardFixture.makeCards(CardFixture.king, CardFixture.six) + ) + dealer.playing(DealerStrategy(BlackjackScore(16)), ShuffledPack) + + dealer.cards.cards shouldContain CardFixture.king + dealer.cards.cards shouldContain CardFixture.six + dealer.countOfCards() shouldBe 3 + } + + "딜러는 카드의 점수 합계가 17점 이상이면 추가로 받을 수 없다" { + val dealer = Dealer( + CardFixture.makeCards(CardFixture.king, CardFixture.seven) + ) + dealer.playing(DealerStrategy(BlackjackScore(17)), ShuffledPack) + + dealer.cards.cards shouldContain CardFixture.king + dealer.cards.cards shouldContain CardFixture.seven + dealer.countOfCards() shouldBe 2 + } + + "딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해야 한다" { + val dealer = Dealer( // 22점 + CardFixture.makeCards(CardFixture.five, CardFixture.seven, CardFixture.king) + ) + val player1 = Player( + "seoul", // 13점 + CardFixture.makeCards(CardFixture.ace1, CardFixture.king, CardFixture.two), status = BlackJackStatus.ALIVE + ) + val player2 = Player( + "wonju", // 21점 + CardFixture.makeCards(CardFixture.queen, CardFixture.king, CardFixture.ace2), status = BlackJackStatus.ALIVE + ) + val actualDealerResult = dealer.dealerResult( + Players(player1, player2) + ) + + actualDealerResult.winningCount shouldBe 0 + actualDealerResult.drawingCount shouldBe 0 + actualDealerResult.losingCount shouldBe 2 + dealer.result(player1) shouldBe PlayableResult.LOSE + dealer.result(player2) shouldBe PlayableResult.LOSE + } + + "딜러와 플레이어 모두가 Burst 상황이라면 경기 결과는 DRAW 이어야한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.nine, CardFixture.three)) + val player1 = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.jack, CardFixture.four), status = BlackJackStatus.ALIVE + ) + val player2 = Player( + "martini", + CardFixture.makeCards(CardFixture.nine, CardFixture.eight, CardFixture.seven), + status = BlackJackStatus.ALIVE + ) + + player1.result(dealer) shouldBe PlayableResult.DRAW + player2.result(dealer) shouldBe PlayableResult.DRAW + dealer.result(player1) shouldBe PlayableResult.DRAW + dealer.result(player2) shouldBe PlayableResult.DRAW + } + "딜러는 플레이어보다 점수가 높은경우 WIN 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.ten)) // 20 + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.five), // 15 + status = BlackJackStatus.ALIVE + ) + + dealer.result(player) shouldBe PlayableResult.WIN + player.result(dealer) shouldBe PlayableResult.LOSE + } + + "딜러는 플레이어보다 점수가 낮은경우 LOSE 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.three, CardFixture.four)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.seven, CardFixture.five), status = BlackJackStatus.ALIVE + ) + + dealer.result(player) shouldBe PlayableResult.LOSE + player.result(dealer) shouldBe PlayableResult.WIN + } + + "딜러는 플레이어와 점수가 같은 경우 DRAW 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.nine, CardFixture.two)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.jack, CardFixture.ace2), status = BlackJackStatus.ALIVE + ) + + dealer.score() shouldBe BlackjackScore(21) + player.score() shouldBe BlackjackScore(21) + player.result(dealer) shouldBe PlayableResult.DRAW + dealer.result(player) shouldBe PlayableResult.DRAW + } + + "딜러가 2승1패인 경우 경기결과가 잘 표현되어야한다" { + val dealer = Dealer( // 15점 + CardFixture.makeCards(CardFixture.two, CardFixture.two, CardFixture.eight, CardFixture.three) + ) + val player1 = Player( + "saml", // 21점 + CardFixture.makeCards(CardFixture.five, CardFixture.king, CardFixture.six), status = BlackJackStatus.ALIVE + ) + val player2 = Player( + "ldap", // 18점 + CardFixture.makeCards(CardFixture.eight, CardFixture.queen, CardFixture.ace2), status = BlackJackStatus.ALIVE + ) + val player3 = Player( + "oauth", // 10점 + CardFixture.makeCards(CardFixture.two, CardFixture.eight), status = BlackJackStatus.ALIVE + ) + val actual = dealer.dealerResult( + Players(player1, player2, player3) + ) + + actual.winningCount shouldBe 1 + actual.drawingCount shouldBe 0 + actual.losingCount shouldBe 2 + } +}) diff --git a/src/test/kotlin/blackjack/model/playable/impl/PlayerTest.kt b/src/test/kotlin/blackjack/model/playable/impl/PlayerTest.kt new file mode 100644 index 0000000000..86b1073f4f --- /dev/null +++ b/src/test/kotlin/blackjack/model/playable/impl/PlayerTest.kt @@ -0,0 +1,92 @@ +package blackjack.model.playable.impl + +import blackjack.model.blackjack.BlackJackStatus +import blackjack.model.card.CardFixture +import blackjack.model.card.pack.impl.ShuffledPack +import blackjack.model.player.BlackjackScore +import blackjack.model.player.playable.impl.Dealer +import blackjack.model.player.playable.impl.Player +import blackjack.model.result.PlayableResult +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class PlayerTest : StringSpec({ + + "플레이어는 dealing 시 2장의 카드를 받을 수 있다" { + shouldNotThrow { + val player = Player("구글", status = BlackJackStatus.ALIVE) + player.dealing(ShuffledPack) + player.cards.count() shouldBe 2 + } + } + + "플레이어는 hit 시 1장의 카드를 받을 수 있다" { + shouldNotThrow { + val player = Player("애플", status = BlackJackStatus.ALIVE) + player.hit(ShuffledPack) + player.cards.count() shouldBe 1 + } + } + + "21이 넘는 점수의 카드를 가지고 있는 경우 Burst 상태로써 BlackJackStatus.DIE 상태 이어야한다" { + val player = Player( + "gin-tonic", + CardFixture.makeCards( + CardFixture.two, + CardFixture.three, + CardFixture.four, + CardFixture.eight, + CardFixture.nine + ), + status = BlackJackStatus.ALIVE + ) + + player.isBurst() shouldBe true + player.status() shouldBe BlackJackStatus.STOP + player.score() shouldBe BlackjackScore(2 + 3 + 4 + 8 + 9) + } + + "플레이어가 BlackJack 이고, 딜러가 Burst 라면 플레이어가 승리하는 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.nine, CardFixture.three)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.jack, CardFixture.ace2), status = BlackJackStatus.ALIVE + ) + + player.result(dealer) shouldBe PlayableResult.WIN + dealer.result(player) shouldBe PlayableResult.LOSE + } + + "플레이어는 딜러보다 점수가 높은 경우 Win 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.three)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.jack), status = BlackJackStatus.ALIVE + ) + + player.result(dealer) shouldBe PlayableResult.WIN + } + + "플레이어는 (Burst 가 아닌 상황에서, burst와 관계없이) 딜러보다 점수가 낮은경우 LOSE 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.ten)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.five), status = BlackJackStatus.ALIVE + ) + val actual = player.result(dealer) + actual shouldBe PlayableResult.LOSE + } + + "플레이어는 딜러와 점수가 같은 경우 DRAW 결과를 반환 해야 한다" { + val dealer = Dealer(CardFixture.makeCards(CardFixture.queen, CardFixture.nine, CardFixture.two)) + val player = Player( + "malibu", + CardFixture.makeCards(CardFixture.ten, CardFixture.jack, CardFixture.ace2), status = BlackJackStatus.ALIVE + ) + + dealer.score() shouldBe BlackjackScore(21) + player.score() shouldBe BlackjackScore(21) + player.result(dealer) shouldBe PlayableResult.DRAW + } +}) diff --git a/src/test/kotlin/study/DefensiveCopyLearn.kt b/src/test/kotlin/study/DefensiveCopyLearn.kt index d8aa5eede9..0bef41a000 100644 --- a/src/test/kotlin/study/DefensiveCopyLearn.kt +++ b/src/test/kotlin/study/DefensiveCopyLearn.kt @@ -1,21 +1,47 @@ package study -import blackjack.model.Card -import blackjack.model.Cards -import blackjack.model.pack.ShuffledPack -import blackjack.view.Console.present import io.kotest.core.spec.style.StringSpec class DefensiveCopyLearn : StringSpec({ - "sdf" { - val list = mutableListOf() - list.add(ShuffledPack.pickCard()) - list.add(ShuffledPack.pickCard()) - list.add(ShuffledPack.pickCard()) - val cards = Cards(list) - println("BEFORE : ${cards.present()}") + "방어적 복사를 하지 않았을 때의 문제가 되는 코드 예제" { + val list = mutableListOf() + list.add("goodNight") + list.add("goodMorning") + list.add("niceYoMeetYou") + val nonDefensiveClass = NonDefensiveClass(list) + println("BEFORE : ${nonDefensiveClass.present()}") list.clear() - println("AFTER : ${cards.present()}") + println("AFTER : ${nonDefensiveClass.present()}") + } + + "방어적 복사를 적용한 코드 예제" { + val list = mutableListOf() + list.add("goodNight") + list.add("goodMorning") + list.add("niceYoMeetYou") + val defensiveClass = DefensiveClass(list.toList()) + println("BEFORE : ${defensiveClass.present()}") + list.clear() + println("AFTER : ${defensiveClass.present()}") } }) + +class NonDefensiveClass( + val strings: MutableList +) { + fun present(): String { + return strings.toString() + } +} + +class DefensiveClass( + strings: List +) { + private val _strings: MutableList = strings.toMutableList() + val strings: List get() = _strings.toList() + + fun present(): String { + return strings.toString() + } +}