diff --git a/README.md b/README.md index e1c7c927d8..294c5461f3 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack +# Step2 +- 기능 목록 + - 도메인 + - 카드 + - 숫자와 문양을 결합한 문자열을 반환 + - 숫자에 맞는 점수 리스트를 반환 + - 카드 A의 경우 1, 11의 점수를 가짐 + - 카드 K, Q, J의 경우 10의 점수를 가짐 + - 카드 풀 + - 랜덤한 카드 한 장을 반환 + - 카드 풀에서 반환한 카드 제거 + - 플레이어 + - 보유한 카드 점수 합 리스트를 반환 + - 추가 카드를 뽑을 수 있는 점수 합인지 검증 + - 블랙잭 게임 + - 플레이어들을 전달 받고 블랙잭 게임 반환 + - 각 개별 플레이어들 블랙젝 게임 실행 + - 입력 + - 참가하는 플레이어의 이름을 입력받음 + - 플레이어들의 이름 문자열이 콤마(,)로 구분되는지 검사 + - 플레이어가 카드를 더 뽑을지 말지를 Y/N으로 입력 + - 입력받는 값이 Y/N인지 검증 + - 출력 + - 플레이어의 이름, 보유 카드 및 플레이어의 점수 출력 \ No newline at end of file diff --git a/src/main/kotlin/blackjack/BlackjackApplication.kt b/src/main/kotlin/blackjack/BlackjackApplication.kt new file mode 100644 index 0000000000..05f4ac15f8 --- /dev/null +++ b/src/main/kotlin/blackjack/BlackjackApplication.kt @@ -0,0 +1,48 @@ +package blackjack + +import blackjack.contoller.BlackjackController +import blackjack.domain.component.BlackjackGameProxy +import blackjack.domain.component.BlackjackInputValidator +import blackjack.domain.model.PlayerName +import blackjack.view.BlackjackInputView +import blackjack.view.BlackjackResultView + +fun main() { + val controller = init() + + process(controller) +} + +private fun init(): BlackjackController { + return BlackjackController( + BlackjackInputView(), + BlackjackInputValidator(), + BlackjackResultView(), + BlackjackGameProxy() + ) +} + +private fun process(blackjackController: BlackjackController) { + val playerNames: List = blackjackController.getPlayerNames() + + blackjackController.initGame(playerNames) + blackjackController.printInitialGameStatus() + + playerNames.forEach { + it.play(blackjackController) + } + + blackjackController.printGameResultStatus() +} + +private fun PlayerName.play(blackjackController: BlackjackController) { + while (blackjackController.getHitPossible(this)) { + if (blackjackController.getHit(this).isNo()) { + return + } + + val player = blackjackController.hit(this) + + blackjackController.printPlayerInfo(player) + } +} diff --git a/src/main/kotlin/blackjack/contoller/BlackjackController.kt b/src/main/kotlin/blackjack/contoller/BlackjackController.kt new file mode 100644 index 0000000000..bbb4167713 --- /dev/null +++ b/src/main/kotlin/blackjack/contoller/BlackjackController.kt @@ -0,0 +1,90 @@ +package blackjack.contoller + +import blackjack.domain.component.BlackjackGameProxy +import blackjack.domain.component.BlackjackInputValidator +import blackjack.domain.model.Player +import blackjack.domain.model.PlayerInfo +import blackjack.domain.model.PlayerName +import blackjack.domain.model.YesNo +import blackjack.view.BlackjackInputView +import blackjack.view.BlackjackResultView + +class BlackjackController( + private val blackJackInputView: BlackjackInputView, + private val blackjackInputValidator: BlackjackInputValidator, + private val blackjackResultView: BlackjackResultView, + private val blackjackGameProxy: BlackjackGameProxy +) { + fun getPlayerNames(): List { + val playerNamesString: String? = blackJackInputView.getPlayerNames() + + return convertPlayerNamesStringToList(playerNamesString) + } + + fun getHitPossible(playerName: PlayerName): Boolean { + return blackjackGameProxy.isHitPossible(playerName) + } + + fun getHit(playerName: PlayerName): YesNo { + return blackJackInputView + .getHit(playerName.name) + .run { convertYesNo(this) } + } + + fun initGame(playerNames: List) { + blackjackGameProxy.init(playerNames) + } + + fun hit(playerName: PlayerName): Player { + return blackjackGameProxy.hit(playerName) + } + + fun printInitialGameStatus() { + blackjackGameProxy + .fetchPlayerInfos() + .run { blackjackResultView.printGameInitialStatus(this) } + } + + fun printGameResultStatus() { + blackjackGameProxy + .fetchPlayerInfos() + .run { blackjackResultView.printGameResultStatus(this) } + } + + fun printPlayerInfos() { + blackjackGameProxy + .fetchPlayerInfos() + .run { blackjackResultView.printPlayerInfos(this) } + } + + fun printPlayerInfo(player: Player) { + val playerInfo = PlayerInfo.from(player) + + blackjackResultView.printPlayerInfo(playerInfo, false) + } + + private fun convertPlayerNamesStringToList(playerNames: String?): List { + return playerNames + .run { blackjackInputValidator.validatePlayerNamesString(this) } + .split(PLAYER_NAME_SEPARATOR) + .run { blackjackInputValidator.validatePlayerNamesSize(this) } + .map { PlayerName(it) } + } + + private fun convertYesNo(yesNo: String?): YesNo { + return yesNo + .run { blackjackInputValidator.validateYesNoString(this) } + .run { YesNo.from(this) ?: YesNo.N } + } + + private fun BlackjackGameProxy.fetchPlayerInfos(): List { + return this + .getPlayers() + .players + .map { PlayerInfo.from(it) } + } + + companion object { + private const val PLAYER_NAME_SEPARATOR = "," + } +} diff --git a/src/main/kotlin/blackjack/domain/component/BlackjackGame.kt b/src/main/kotlin/blackjack/domain/component/BlackjackGame.kt new file mode 100644 index 0000000000..2137fed8e5 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/component/BlackjackGame.kt @@ -0,0 +1,49 @@ +package blackjack.domain.component + +import blackjack.domain.model.* + +class BlackjackGame private constructor( + val players: Players, + private val cardPool: CardPool +) { + fun isHitPossible(playerName: PlayerName): Boolean { + return players + .findByName(playerName) + .checkNull() + .isPossibleToHit() + } + + fun hit(playerName: PlayerName): Player { + val player = players.findByName(playerName) + val card = cardPool.pickAndRemove() + + return player + .checkNull() + .append(card) + } + + private fun Player?.checkNull(): Player { + check(this != null) { "해당 플레이어는 존재하지 않습니다." } + + return this + } + + companion object { + const val INITIAL_CARD_COUNT = 2 + + fun create(playerNames: List): BlackjackGame { + val cardPool = CardPool.create() + val players = playerNames + .map { Player(it, fetchInitialCards(cardPool)) } + .run { Players(this) } + + return BlackjackGame(players, cardPool) + } + + private fun fetchInitialCards(cardPool: CardPool): Cards { + return cardPool + .pickAndRemove(INITIAL_CARD_COUNT) + .run { Cards.of(this) } + } + } +} diff --git a/src/main/kotlin/blackjack/domain/component/BlackjackGameProxy.kt b/src/main/kotlin/blackjack/domain/component/BlackjackGameProxy.kt new file mode 100644 index 0000000000..bdbc87718e --- /dev/null +++ b/src/main/kotlin/blackjack/domain/component/BlackjackGameProxy.kt @@ -0,0 +1,37 @@ +package blackjack.domain.component + +import blackjack.domain.model.Player +import blackjack.domain.model.PlayerName +import blackjack.domain.model.Players + +class BlackjackGameProxy( + private var blackjackGame: BlackjackGame? = null +) { + fun init(playerNames: List) { + blackjackGame = BlackjackGame.create(playerNames) + } + + fun isHitPossible(playerName: PlayerName): Boolean { + return blackjackGame + .checkGameExist() + .isHitPossible(playerName) + } + + fun hit(playerName: PlayerName): Player { + return blackjackGame + .checkGameExist() + .hit(playerName) + } + + fun getPlayers(): Players { + return blackjackGame + .checkGameExist() + .players + } + + private fun BlackjackGame?.checkGameExist(): BlackjackGame { + check(this != null) { "게임이 존재하지 않습니다." } + + return this + } +} diff --git a/src/main/kotlin/blackjack/domain/component/BlackjackInputValidator.kt b/src/main/kotlin/blackjack/domain/component/BlackjackInputValidator.kt new file mode 100644 index 0000000000..9433213d7e --- /dev/null +++ b/src/main/kotlin/blackjack/domain/component/BlackjackInputValidator.kt @@ -0,0 +1,21 @@ +package blackjack.domain.component + +class BlackjackInputValidator { + fun validatePlayerNamesString(playerNames: String?): String { + require(!playerNames.isNullOrBlank()) { "플레이어 이름은 null 또는 공백일 수 없습니다." } + + return playerNames + } + + fun validatePlayerNamesSize(playerNames: List): List { + require(playerNames.isNotEmpty()) { "게임에는 1명 이상의 플레이어가 참가해야합니다." } + + return playerNames + } + + fun validateYesNoString(yesNo: String?): String { + require(!yesNo.isNullOrBlank()) { "힛은 null 또는 공백일 수 없습니다." } + + return yesNo + } +} diff --git a/src/main/kotlin/blackjack/domain/model/Card.kt b/src/main/kotlin/blackjack/domain/model/Card.kt new file mode 100644 index 0000000000..f7740da600 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/Card.kt @@ -0,0 +1,24 @@ +package blackjack.domain.model + +class Card private constructor( + val number: CardNumber, + val shape: CardShape, + val scores: List +) { + companion object { + private val CARD_ACE_SCORES = listOf(Score(1), Score(11)) + private val CARD_KQJ_SCORES = listOf(Score(10)) + + fun of(number: CardNumber, shape: CardShape): Card { + if (number.isAce()) { + return Card(number, shape, CARD_ACE_SCORES) + } + + if (number.isOneOfKQJ()) { + return Card(number, shape, CARD_KQJ_SCORES) + } + + return Card(number, shape, listOf(Score(number.number))) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/model/CardNumber.kt b/src/main/kotlin/blackjack/domain/model/CardNumber.kt new file mode 100644 index 0000000000..3b18aeec9f --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/CardNumber.kt @@ -0,0 +1,26 @@ +package blackjack.domain.model + +enum class CardNumber(val number: Int, val displayName: String) { + ACE(1, "A"), + 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(11, "J"), + QUEEN(12, "Q"), + KING(13, "K"), + ; + + fun isAce(): Boolean { + return number == 1 + } + + fun isOneOfKQJ(): Boolean { + return number >= 11 + } +} diff --git a/src/main/kotlin/blackjack/domain/model/CardPool.kt b/src/main/kotlin/blackjack/domain/model/CardPool.kt new file mode 100644 index 0000000000..1efba02d67 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/CardPool.kt @@ -0,0 +1,25 @@ +package blackjack.domain.model + +class CardPool private constructor(private var cards: MutableList) { + fun pickAndRemove(): Card { + check(cards.size > 0) { "카드 풀의 카드가 존재하지 않습니다." } + + val target = (0 until cards.size).random() + + return cards.removeAt(target) + } + + fun pickAndRemove(count: Int): List { + if (count < 0) { + return listOf() + } + + return (0 until count).map { pickAndRemove() } + } + + companion object { + fun create(): CardPool { + return CardPool(Cards.create().cards.toMutableList()) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/model/CardShape.kt b/src/main/kotlin/blackjack/domain/model/CardShape.kt new file mode 100644 index 0000000000..a09507dcbb --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/CardShape.kt @@ -0,0 +1,8 @@ +package blackjack.domain.model + +enum class CardShape(val shape: String) { + Spade("스페이드"), + Clover("클로버"), + Heart("하트"), + Diamond("다이아몬드"), +} diff --git a/src/main/kotlin/blackjack/domain/model/Cards.kt b/src/main/kotlin/blackjack/domain/model/Cards.kt new file mode 100644 index 0000000000..8c1d9d8ab6 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/Cards.kt @@ -0,0 +1,70 @@ +package blackjack.domain.model + +class Cards(val cards: List) { + fun scores(): List { + check(cards.isNotEmpty()) { "카드의 개수가 0장으로 점수를 계산할 수 없습니다." } + + return cards + .drop(1) + .fold(cards.getFirstCardScores()) { scores, card -> calculatePossibleScores(card, scores) } + .toList() + .sortedBy { it.score } + } + + private fun List.getFirstCardScores(): Set { + return first() + .scores + .toSet() + } + + private fun calculatePossibleScores(card: Card, scores: Set): Set { + return card.scores + .map { scores.map { score -> score + it } } + .flatten() + .toSet() + } + + fun isPossibleToHit(): Boolean { + val scores = scores() + + if (scores.last().isWinningScore()) { + return false + } + + return scores.first().ltWinningScore() + } + + fun getFinalScore(): Score { + val scores = scores() + + if (scores.first().isBust()) { + return scores.first() + } + + return scores.last { it.lteWinningScore() } + } + + operator fun plus(card: Card): Cards { + return of(cards + listOf(card)) + } + + companion object { + fun create(): Cards { + val numbers = CardNumber.values() + val shapes = CardShape.values() + + return shapes + .map { shape -> numbers.map { number -> Card.of(number, shape) } } + .flatten() + .run { Cards(this) } + } + + fun of(vararg cards: Card): Cards { + return Cards(cards.toList()) + } + + fun of(cards: List): Cards { + return Cards(cards) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/model/Player.kt b/src/main/kotlin/blackjack/domain/model/Player.kt new file mode 100644 index 0000000000..49812b20ca --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/Player.kt @@ -0,0 +1,25 @@ +package blackjack.domain.model + +class Player( + val name: PlayerName, + private var cards: Cards +) { + + fun isPossibleToHit(): Boolean { + return cards.isPossibleToHit() + } + + fun append(card: Card): Player { + cards += card + + return this + } + + fun cards(): Cards { + return cards + } + + fun getFinalScore(): Score { + return cards.getFinalScore() + } +} diff --git a/src/main/kotlin/blackjack/domain/model/PlayerInfo.kt b/src/main/kotlin/blackjack/domain/model/PlayerInfo.kt new file mode 100644 index 0000000000..5fe73bc0a0 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/PlayerInfo.kt @@ -0,0 +1,23 @@ +package blackjack.domain.model + +data class PlayerInfo( + val name: String, + val cards: List, + val score: Int +) { + companion object { + fun from(player: Player): PlayerInfo { + return PlayerInfo( + player.name.name, + player.getCardsString(), + player.getFinalScore().score + ) + } + + private fun Player.getCardsString(): List { + return cards() + .cards + .map { "${it.number.displayName}${it.shape.shape}" } + } + } +} diff --git a/src/main/kotlin/blackjack/domain/model/PlayerName.kt b/src/main/kotlin/blackjack/domain/model/PlayerName.kt new file mode 100644 index 0000000000..7e2af7faf4 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/PlayerName.kt @@ -0,0 +1,4 @@ +package blackjack.domain.model + +@JvmInline +value class PlayerName(val name: String) diff --git a/src/main/kotlin/blackjack/domain/model/Players.kt b/src/main/kotlin/blackjack/domain/model/Players.kt new file mode 100644 index 0000000000..704a49da48 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/Players.kt @@ -0,0 +1,7 @@ +package blackjack.domain.model + +class Players(val players: List) { + fun findByName(playerName: PlayerName): Player? { + return players.find { it.name == playerName } + } +} diff --git a/src/main/kotlin/blackjack/domain/model/Score.kt b/src/main/kotlin/blackjack/domain/model/Score.kt new file mode 100644 index 0000000000..1cf254906d --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/Score.kt @@ -0,0 +1,40 @@ +package blackjack.domain.model + +@JvmInline +value class Score(val score: Int) : Comparable { + fun isBust(): Boolean { + return score > WINNING_SCORE + } + + fun isWinningScore(): Boolean { + return score == WINNING_SCORE + } + + fun ltWinningScore(): Boolean { + return score < WINNING_SCORE + } + + fun lteWinningScore(): Boolean { + return score <= WINNING_SCORE + } + + operator fun plus(score: Score): Score { + return Score(this.score + score.score) + } + + operator fun minus(score: Int): Score { + return Score(this.score - score) + } + + operator fun compareTo(score: Int): Int { + return this.score - score + } + + override fun compareTo(other: Score): Int { + return this.score - other.score + } + + companion object { + private const val WINNING_SCORE = 21 + } +} diff --git a/src/main/kotlin/blackjack/domain/model/YesNo.kt b/src/main/kotlin/blackjack/domain/model/YesNo.kt new file mode 100644 index 0000000000..c9be02e298 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/model/YesNo.kt @@ -0,0 +1,15 @@ +package blackjack.domain.model + +enum class YesNo(val value: String) { + Y("y"), N("n"); + + fun isNo(): Boolean { + return this == N + } + + companion object { + fun from(value: String): YesNo? { + return values().find { it.value == value } + } + } +} diff --git a/src/main/kotlin/blackjack/view/BlackjackInputView.kt b/src/main/kotlin/blackjack/view/BlackjackInputView.kt new file mode 100644 index 0000000000..28fd7bc72f --- /dev/null +++ b/src/main/kotlin/blackjack/view/BlackjackInputView.kt @@ -0,0 +1,17 @@ +package blackjack.view + +class BlackjackInputView { + fun getPlayerNames(): String? { + return getInput("플레이어 이름을 입력하세요. (쉼표 기준 분리)") + } + + fun getHit(playerName: String): String? { + return getInput("$playerName, 한 장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + } + + private fun getInput(message: String): String? { + println(message) + + return readlnOrNull() + } +} diff --git a/src/main/kotlin/blackjack/view/BlackjackResultView.kt b/src/main/kotlin/blackjack/view/BlackjackResultView.kt new file mode 100644 index 0000000000..26cbc50b3c --- /dev/null +++ b/src/main/kotlin/blackjack/view/BlackjackResultView.kt @@ -0,0 +1,39 @@ +package blackjack.view + +import blackjack.domain.component.BlackjackGame +import blackjack.domain.model.PlayerInfo + +class BlackjackResultView { + fun printGameInitialStatus(playerInfos: List) { + println("${playerInfos.createPlayerNamesString()}에게 ${BlackjackGame.INITIAL_CARD_COUNT}장의 나누었습니다.") + printPlayerInfos(playerInfos) + } + + fun printGameResultStatus(playerInfos: List) { + println("----- 게임 결과") + printPlayerInfos(playerInfos, true) + } + + fun printPlayerInfos(playerInfos: List, withScore: Boolean = false) { + playerInfos.forEach { printPlayerInfo(it, withScore) } + println() + } + + fun printPlayerInfo(playerInfo: PlayerInfo, withScore: Boolean = false) { + val message = "${playerInfo.name} 카드: ${playerInfo.createCardsString()}" + if (withScore) { " 결과 - ${playerInfo.score}" } else { "" } + + println(message) + } + + private fun List.createPlayerNamesString(): String { + return joinToString(CARDS_STRING_DELIMITER) { it.name } + } + + private fun PlayerInfo.createCardsString(): String { + return this.cards.joinToString(CARDS_STRING_DELIMITER) + } + + companion object { + private const val CARDS_STRING_DELIMITER = ", " + } +} diff --git a/src/test/kotlin/blackjack/domain/component/BlackjackInputValidatorTest.kt b/src/test/kotlin/blackjack/domain/component/BlackjackInputValidatorTest.kt new file mode 100644 index 0000000000..1b359ee7c9 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/component/BlackjackInputValidatorTest.kt @@ -0,0 +1,36 @@ +package blackjack.domain.component + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.forAll +import io.kotest.data.row + +class BlackjackInputValidatorTest : FunSpec({ + val validator = BlackjackInputValidator() + + test("플레이어 이름이 null 또는 공백인 경우 IllegalArgumentException 발생 테스트") { + forAll( + row(null), + row(""), + row(" "), + ) { playerNamesString -> + shouldThrow { validator.validatePlayerNamesString(playerNamesString) } + } + } + + test("플레이어 이름 목록이 존재하지 않을 경우 IllegalArgumentException 테스트") { + val input = listOf() + + shouldThrow { validator.validatePlayerNamesSize(input) } + } + + test("예/아니오 입력이 null이거나 공백인 경우 IllegalArgumentException 테스트") { + forAll( + row(null), + row(""), + row(" "), + ) { playerNamesString -> + shouldThrow { validator.validateYesNoString(playerNamesString) } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/model/CardPoolTest.kt b/src/test/kotlin/blackjack/domain/model/CardPoolTest.kt new file mode 100644 index 0000000000..ae0a96428c --- /dev/null +++ b/src/test/kotlin/blackjack/domain/model/CardPoolTest.kt @@ -0,0 +1,17 @@ +package blackjack.domain.model + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec + +class CardPoolTest : FunSpec({ + test("카드 풀을 모두 소진 후 뽑기 시도 시 IllegalStateException 예외 발생 테스트") { + val cardPoolSize = Cards.create().cards.size + val cardPool = CardPool.create() + + shouldThrow { + repeat(cardPoolSize + 1) { + cardPool.pickAndRemove() + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/model/CardTest.kt b/src/test/kotlin/blackjack/domain/model/CardTest.kt new file mode 100644 index 0000000000..539cc5385c --- /dev/null +++ b/src/test/kotlin/blackjack/domain/model/CardTest.kt @@ -0,0 +1,44 @@ +package blackjack.domain.model + +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe + +class CardTest : FunSpec({ + test("카드 숫자가 2~10일 경우 점수가 1개이며 점수가 10인지 테스트") { + forAll( + row(Card.of(CardNumber.TWO, CardShape.Spade), Score(2)), + row(Card.of(CardNumber.THREE, CardShape.Spade), Score(3)), + row(Card.of(CardNumber.FOUR, CardShape.Spade), Score(4)), + row(Card.of(CardNumber.FIVE, CardShape.Spade), Score(5)), + row(Card.of(CardNumber.SIX, CardShape.Spade), Score(6)), + row(Card.of(CardNumber.SEVEN, CardShape.Spade), Score(7)), + row(Card.of(CardNumber.EIGHT, CardShape.Spade), Score(8)), + row(Card.of(CardNumber.NINE, CardShape.Spade), Score(9)), + row(Card.of(CardNumber.TEN, CardShape.Spade), Score(10)), + ) { card, answer -> + card.scores.size shouldBe 1 + card.scores[0] shouldBe answer + } + } + + test("카드 숫자가 ACE일 경우 점수가 2개이고 점수가 1, 11인지 테스트") { + val card = Card.of(CardNumber.ACE, CardShape.Spade) + + card.scores.size shouldBe 2 + card.scores[0] shouldBe Score(1) + card.scores[1] shouldBe Score(11) + } + + test("카드 숫자가 K, Q, J일 경우 점수가 1개이고 점수가 10인지 테스트") { + forAll( + row(Card.of(CardNumber.JACK, CardShape.Spade)), + row(Card.of(CardNumber.QUEEN, CardShape.Spade)), + row(Card.of(CardNumber.KING, CardShape.Spade)), + ) { card -> + card.scores.size shouldBe 1 + card.scores[0] shouldBe Score(10) + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/model/CardsTest.kt b/src/test/kotlin/blackjack/domain/model/CardsTest.kt new file mode 100644 index 0000000000..6994ce094f --- /dev/null +++ b/src/test/kotlin/blackjack/domain/model/CardsTest.kt @@ -0,0 +1,59 @@ +package blackjack.domain.model + +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe + +class CardsTest : FunSpec({ + test("카드 덱 생성 정상 반환 테스트") { + val cards = Cards.create() + val answer = CardShape.values().size * CardNumber.values().size + + cards.cards.size shouldBe answer + } + + test("카드 점수의 합 정상 반환 테스트") { + val ace = Card.of(CardNumber.ACE, CardShape.Spade) + val two = Card.of(CardNumber.TWO, CardShape.Spade) + val three = Card.of(CardNumber.THREE, CardShape.Spade) + val jack = Card.of(CardNumber.JACK, CardShape.Spade) + val queen = Card.of(CardNumber.QUEEN, CardShape.Spade) + val king = Card.of(CardNumber.KING, CardShape.Spade) + + forAll( + row(Cards.of(ace, two), listOf(Score(3), Score(13))), + row(Cards.of(ace, three), listOf(Score(4), Score(14))), + row(Cards.of(ace, jack), listOf(Score(11), Score(21))), + row(Cards.of(ace, queen), listOf(Score(11), Score(21))), + row(Cards.of(ace, king), listOf(Score(11), Score(21))), + + row(Cards.of(two, three), listOf(Score(5))), + + row(Cards.of(two, jack), listOf(Score(12))), + row(Cards.of(two, king), listOf(Score(12))), + row(Cards.of(two, queen), listOf(Score(12))), + + row(Cards.of(jack, queen), listOf(Score(20))), + row(Cards.of(queen, king), listOf(Score(20))), + row(Cards.of(jack, king), listOf(Score(20))), + ) { cards, answer -> + cards.scores() shouldBe answer + } + } + + test("힛 가능 여부 정상 반환 테스트") { + val ace = Card.of(CardNumber.ACE, CardShape.Spade) + val two = Card.of(CardNumber.TWO, CardShape.Spade) + val jack = Card.of(CardNumber.JACK, CardShape.Spade) + val queen = Card.of(CardNumber.QUEEN, CardShape.Spade) + + forAll( + row(Cards.of(ace, two), true), + row(Cards.of(ace, jack), false), + row(Cards.of(two, jack, queen), false), + ) { cards, answer -> + cards.isPossibleToHit() shouldBe answer + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/model/PlayerTest.kt b/src/test/kotlin/blackjack/domain/model/PlayerTest.kt new file mode 100644 index 0000000000..c7e51e992a --- /dev/null +++ b/src/test/kotlin/blackjack/domain/model/PlayerTest.kt @@ -0,0 +1,82 @@ +package blackjack.domain.model + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class PlayerTest : FunSpec({ + test("점수 합이 21 미만일 경우 카드 뽑기 가능 여부 메소드 true 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.KING, CardShape.Spade), + Card.of(CardNumber.JACK, CardShape.Spade) + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe true + } + + test("점수 합이 21일 경우 카드 뽑기 가능 여부 메소드 false 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.TWO, CardShape.Spade), + Card.of(CardNumber.NINE, CardShape.Spade), + Card.of(CardNumber.TEN, CardShape.Spade), + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe false + } + + test("점수 합이 21 초과일 경우 카드 뽑기 가능 여부 메소드 false 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.THREE, CardShape.Spade), + Card.of(CardNumber.NINE, CardShape.Spade), + Card.of(CardNumber.TEN, CardShape.Spade), + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe false + } + + test("카드 A를 포함한 상태에서 점수 합이 21 미만이 가능할 경우 카드 뽑기 가능 여부 메소드 true 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.ACE, CardShape.Spade), + Card.of(CardNumber.TWO, CardShape.Spade), + Card.of(CardNumber.TEN, CardShape.Spade) + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe true + } + + test("카드 A를 포함한 상태에서 점수 합이 21인 경우 카드 뽑기 가능 여부 메소드 false 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.ACE, CardShape.Spade), + Card.of(CardNumber.JACK, CardShape.Spade), + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe false + } + + test("카드 A를 포함한 상태에서 점수 합이 반드시 21 이상일 경우 카드 뽑기 가능 여부 메소드 false 반환 테스트") { + val name = PlayerName("홍길동") + val cards = Cards.of( + Card.of(CardNumber.ACE, CardShape.Spade), + Card.of(CardNumber.TEN, CardShape.Spade), + Card.of(CardNumber.JACK, CardShape.Spade), + ) + + val player = Player(name, cards) + + player.isPossibleToHit() shouldBe false + } +}) diff --git a/src/test/kotlin/blackjack/domain/model/YesNoTest.kt b/src/test/kotlin/blackjack/domain/model/YesNoTest.kt new file mode 100644 index 0000000000..c24934da8d --- /dev/null +++ b/src/test/kotlin/blackjack/domain/model/YesNoTest.kt @@ -0,0 +1,24 @@ +package blackjack.domain.model + +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe + +class YesNoTest : FunSpec({ + test("y 또는 n일 경우 정상 반환 테스트") { + forAll( + row("y", YesNo.Y), + row("n", YesNo.N), + ) { yesNo, answer -> + YesNo.from(yesNo) shouldBe answer + } + } + + test("y/n 가 아닐 경우 null 반환 테스트") { + val input = "a" + val result = YesNo.from(input) + + result shouldBe null + } +})