diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..825576b69 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,60 @@ +# 기능 요구 사항 +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. +- 게임을 완료한 후 각 플레이어별로 승패를 출력한다. + +# 기능 목록 +- 카드 모양 +- [x] 카드 모양에는 스페이드, 다이아몬드, 하트, 클로버가 있다. + +- 카드 숫자 +- [x] 카드 숫자의 범위는 1부터 13이다. +- [x] 카드 숫자를 문자로 반환한다. + - [x] 1, 11, 12, 13은 각각 A, J, Q, K로 반환한다. + - [x] 2 ~ 10은 그대로 반환한다. +- [x] 카드 숫자를 정수형으로 반환한다. + +- 카드 +- [x] 카드는 각 모양별로 2부터 10, A, J, Q, K가 존재한다. + +- 플레이어의 카드 목록 +- [x] 카드 목록에 카드를 추가한다. +- [x] 총 점수를 반환한다. + - [x] 단, A는 기존 총 점수에 11점을 더한 값이 21점 이하이면 11점으로 계산하고, 21점을 초과하면 1점으로 계산한다. + - [x] 단, J, Q, K는 모두 10점으로 계산한다. + - [x] 2 ~ 10은 그대로 계산한다. + +- 카드덱 +- [x] 카드 하나를 뽑는다. + - [x] 카드덱은 아직 뽑히지 않은 카드를 갖는다. + - [x] 뽑은 카드는 아직 뽑히지 않은 카드 중 하나여야 한다. + +- 플레이어 +- [x] 플레이어는 이름을 갖는다. +- [x] 플레이어는 카드 목록에 카드를 추가한다. + - [x] 플레이어는 자신이 뽑은 카드 목록을 갖는다. +- [x] 플레이어는 카드의 합이 21을 초과하지 않는지 확인한다. +- [x] 자신의 점수를 반환한다. +- [x] 자신이 가진 카드를 반환한다. + +- 플레이어들 +- [x] 모든 플레이어의 카드를 2장씩 뽑는다. + +- 딜러 +- [x] 딜러는 스테이인지 확인한다. + - [x] 카드의 합이 17점 이상이면 스테이이다. + - [x] 카드의 합이 16점 이하이면 스테이가 아니다. +- [x] 딜러는 자신이 보유한 첫번째 카드를 반환한다. + +- 승부 결과 +- [x] 플레이어의 승패를 결정한다. + - [x] 플레이어가 21점을 초과하면 패배한다. + - [x] 딜러만 21점을 초과하면 플레이어가 승리한다. + - [x] 딜러와 플레이어 모두 21점을 초과하지 않고 플레이어가 딜러보다 점수가 높으면 플레이어가 승리한다. + - [x] 딜러와 플레이어 모두 21점을 초과하지 않고 플레이어와 딜러의 점수가 같으면 무승부이다. + - [x] 딜러와 플레이어 모두 21점을 초과하지 않고 딜러가 플레이어보다 점수가 높으면 플레이어가 패배한다. + +- 명령어 +- [x] 명령어는 y 또는 n이다. + - [x] 단, 대소문자를 구분하지 않는다. \ No newline at end of file diff --git a/src/main/kotlin/.gitkeep b/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/kotlin/blackjack/Application.kt b/src/main/kotlin/blackjack/Application.kt new file mode 100644 index 000000000..245b4428f --- /dev/null +++ b/src/main/kotlin/blackjack/Application.kt @@ -0,0 +1,7 @@ +package blackjack + +import blackjack.controller.BlackjackController + +fun main() { + BlackjackController().start() +} diff --git a/src/main/kotlin/blackjack/controller/BlackjackController.kt b/src/main/kotlin/blackjack/controller/BlackjackController.kt new file mode 100644 index 000000000..b1374156f --- /dev/null +++ b/src/main/kotlin/blackjack/controller/BlackjackController.kt @@ -0,0 +1,36 @@ +package blackjack.controller + +import blackjack.domain.Blackjack +import blackjack.domain.CardDeck +import blackjack.domain.Player +import blackjack.view.InputView +import blackjack.view.OutputView + +class BlackjackController { + fun start() { + with(initBlackjack()) { + setUpCard(this) + + val result = start(onDrawn = OutputView::printDrawn) + OutputView.printBlackjackResult(result) + } + } + + private fun initBlackjack(): Blackjack = Blackjack(CardDeck(), enrollPlayers()) + + private fun enrollPlayers(): List = InputView.inputNames().map { name -> + Player(name, needToDraw = { + InputView.inputDrawCommand(name) + }) + } + + private fun setUpCard(blackJack: Blackjack) { + drawInitialCards(blackJack) + OutputView.printFirstOpenCards(blackJack.getFirstOpenCards()) + OutputView.printInterval() + } + + private fun drawInitialCards(blackJack: Blackjack) { + blackJack.readyToStart() + } +} diff --git a/src/main/kotlin/blackjack/domain/Blackjack.kt b/src/main/kotlin/blackjack/domain/Blackjack.kt new file mode 100644 index 000000000..641de3f34 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Blackjack.kt @@ -0,0 +1,21 @@ +package blackjack.domain + +class Blackjack(private val deck: CardDeck, private val participants: Participants) { + constructor(deck: CardDeck, players: List) : this(deck, Participants(listOf(Dealer()) + players)) + + fun readyToStart() { + participants.drawFirst(deck) + } + + fun start(onDrawn: (Participant) -> Unit): BlackjackResult { + participants.takePlayerTurns(deck, onDrawn) + participants.takeDealerTurns(deck, onDrawn) + + return BlackjackResult( + participants.getCardResults(), + participants.getMatchResults(), + ) + } + + fun getFirstOpenCards(): Map> = participants.getFirstOpenCards() +} diff --git a/src/main/kotlin/blackjack/domain/BlackjackResult.kt b/src/main/kotlin/blackjack/domain/BlackjackResult.kt new file mode 100644 index 000000000..48a1325ba --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackjackResult.kt @@ -0,0 +1,6 @@ +package blackjack.domain + +data class BlackjackResult( + val cardResults: List, + val matchResults: List, +) diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt new file mode 100644 index 000000000..19f038d9e --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Card.kt @@ -0,0 +1,17 @@ +package blackjack.domain + +data class Card(val number: CardNumber, val suit: Suit) { + fun getScore(): Int = number.score + + fun isAce(): Boolean = number == CardNumber.ACE + + companion object { + private val CARDS = Suit.values().flatMap { suit -> + CardNumber.values().map { cardNumber -> + Card(cardNumber, suit) + } + } + + fun all(): List = CARDS.toList() + } +} diff --git a/src/main/kotlin/blackjack/domain/CardDeck.kt b/src/main/kotlin/blackjack/domain/CardDeck.kt new file mode 100644 index 000000000..e0f8e6a2a --- /dev/null +++ b/src/main/kotlin/blackjack/domain/CardDeck.kt @@ -0,0 +1,9 @@ +package blackjack.domain + +class CardDeck(deck: List = Card.all().shuffled()) { + constructor(vararg cards: Card) : this(cards.toList()) + + private val deck: MutableList = deck.toMutableList() + + fun draw(): Card = deck.removeFirst() +} diff --git a/src/main/kotlin/blackjack/domain/CardNumber.kt b/src/main/kotlin/blackjack/domain/CardNumber.kt new file mode 100644 index 000000000..abe5438a9 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/CardNumber.kt @@ -0,0 +1,17 @@ +package blackjack.domain + +enum class CardNumber(val score: Int) { + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10), + QUEEN(10), + KING(10); +} diff --git a/src/main/kotlin/blackjack/domain/CardResult.kt b/src/main/kotlin/blackjack/domain/CardResult.kt new file mode 100644 index 000000000..fa14c05ca --- /dev/null +++ b/src/main/kotlin/blackjack/domain/CardResult.kt @@ -0,0 +1,7 @@ +package blackjack.domain + +data class CardResult( + val participant: Participant, + val cards: List, + val scoreSum: Int, +) diff --git a/src/main/kotlin/blackjack/domain/Cards.kt b/src/main/kotlin/blackjack/domain/Cards.kt new file mode 100644 index 000000000..f388b1bdf --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Cards.kt @@ -0,0 +1,39 @@ +package blackjack.domain + +class Cards(vararg cards: Card) { + private val _items: MutableList by lazy { mutableListOf() } + val items: List + get() = _items.toList() + + init { + _items.addAll(cards) + } + + fun add(card: Card) { + _items.add(card) + } + + fun getFirstCard(): Card = _items.first() + + fun calculateTotalScore(): Int { + val score = _items.sumOf(Card::getScore) + return calculateAceScore(score) + } + + private fun calculateAceScore(score: Int): Int = + if (hasAce() && !isOverBlackjack(score + BONUS_SCORE)) score + BONUS_SCORE else score + + fun isOverBlackjack(): Boolean = calculateTotalScore() > BLACKJACK_SCORE + + private fun isOverBlackjack(score: Int): Boolean = score > BLACKJACK_SCORE + + fun isStay(): Boolean = calculateTotalScore() >= STAY_SCORE + + private fun hasAce(): Boolean = _items.any(Card::isAce) + + companion object { + private const val BONUS_SCORE = 10 + private const val BLACKJACK_SCORE = 21 + private const val STAY_SCORE = 17 + } +} diff --git a/src/main/kotlin/blackjack/domain/Dealer.kt b/src/main/kotlin/blackjack/domain/Dealer.kt new file mode 100644 index 000000000..05645eb20 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Dealer.kt @@ -0,0 +1,11 @@ +package blackjack.domain + +class Dealer : Participant(DEALER_NAME) { + override fun getFirstOpenCards(): List = listOf(getFirstCard()) + + override fun canDraw(): Boolean = !isStay() + + companion object { + private const val DEALER_NAME = "딜러" + } +} diff --git a/src/main/kotlin/blackjack/domain/GameResult.kt b/src/main/kotlin/blackjack/domain/GameResult.kt new file mode 100644 index 000000000..ac9638088 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/GameResult.kt @@ -0,0 +1,5 @@ +package blackjack.domain + +enum class GameResult { + WIN, DRAW, LOSE; +} diff --git a/src/main/kotlin/blackjack/domain/MatchResult.kt b/src/main/kotlin/blackjack/domain/MatchResult.kt new file mode 100644 index 000000000..8cf1a06a6 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/MatchResult.kt @@ -0,0 +1,8 @@ +package blackjack.domain + +data class MatchResult( + val participant: Participant, + val winCount: Int, + val loseCount: Int, + val drawCount: Int +) diff --git a/src/main/kotlin/blackjack/domain/Participant.kt b/src/main/kotlin/blackjack/domain/Participant.kt new file mode 100644 index 000000000..9a8eaf885 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Participant.kt @@ -0,0 +1,32 @@ +package blackjack.domain + +abstract class Participant(val name: String) { + private val cards = Cards() + + abstract fun getFirstOpenCards(): List + + abstract fun canDraw(): Boolean + + fun getTotalScore(): Int = cards.calculateTotalScore() + + fun isBust(): Boolean = cards.isOverBlackjack() + + fun isStay(): Boolean = cards.isStay() + + infix fun judge(other: Participant): GameResult = when { + isBust() && other.isBust() -> GameResult.DRAW + isBust() -> GameResult.LOSE + other.isBust() -> GameResult.WIN + getTotalScore() == other.getTotalScore() -> GameResult.DRAW + getTotalScore() > other.getTotalScore() -> GameResult.WIN + else -> GameResult.LOSE + } + + fun addCard(card: Card) { + cards.add(card) + } + + fun getCards(): List = cards.items + + fun getFirstCard(): Card = cards.getFirstCard() +} diff --git a/src/main/kotlin/blackjack/domain/Participants.kt b/src/main/kotlin/blackjack/domain/Participants.kt new file mode 100644 index 000000000..86f9034a1 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Participants.kt @@ -0,0 +1,84 @@ +package blackjack.domain + +class Participants(private val participants: List) { + init { + require(participants.size in MINIMUM_PARTICIPANTS..MAXIMUM_PARTICIPANTS) { + "블랙잭은 딜러를 포함하여 최소 ${MINIMUM_PARTICIPANTS}명에서 최대 ${MAXIMUM_PARTICIPANTS}명의 플레이어가 참여 가능합니다. (현재 플레이어수 : ${participants.size}명)" + } + } + + fun drawFirst(deck: CardDeck) { + participants.forEach { participant -> + participant.addCard(deck.draw()) + participant.addCard(deck.draw()) + } + } + + fun takePlayerTurns(deck: CardDeck, onDrawn: (Participant) -> Unit) { + getPlayers().forEach { participant -> + drawUntilCanDraw(participant, deck, onDrawn) + } + } + + fun takeDealerTurns(deck: CardDeck, onDrawn: (Participant) -> Unit) { + drawUntilCanDraw(getDealer(), deck, onDrawn) + } + + private fun drawUntilCanDraw( + participant: Participant, + deck: CardDeck, + onDrawn: (Participant) -> Unit + ) { + while (participant.canDraw()) { + draw(participant, deck) + onDrawn(participant) + } + } + + fun getMatchResults(): List = listOf(getDealerMatchResult()) + getPlayerMatchResults() + + private fun getDealerMatchResult(): MatchResult { + var (win, lose, draw) = Triple(0, 0, 0) + getPlayers().forEach { player -> + when (getDealer() judge player) { + GameResult.WIN -> win++ + GameResult.LOSE -> lose++ + GameResult.DRAW -> draw++ + } + } + return MatchResult(getDealer(), win, lose, draw) + } + + private fun getPlayerMatchResults(): List = getPlayers().map { player -> + var (win, lose, draw) = Triple(0, 0, 0) + when (player judge getDealer()) { + GameResult.WIN -> win++ + GameResult.LOSE -> lose++ + GameResult.DRAW -> draw++ + } + MatchResult(player, win, lose, draw) + } + + fun getCardResults(): List = participants.map { participant -> + CardResult( + participant, + participant.getCards(), + participant.getTotalScore() + ) + } + + fun getFirstOpenCards(): Map> = participants.associate { it.name to it.getFirstOpenCards() } + + private fun getPlayers(): List = participants.filterIsInstance() + + private fun getDealer(): Participant = participants.first { it is Dealer } + + private fun draw(participant: Participant, deck: CardDeck) { + participant.addCard(deck.draw()) + } + + companion object { + private const val MINIMUM_PARTICIPANTS = 2 + private const val MAXIMUM_PARTICIPANTS = 8 + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt new file mode 100644 index 000000000..f97287b1d --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -0,0 +1,7 @@ +package blackjack.domain + +class Player(name: String, private val needToDraw: () -> Boolean = { true }) : Participant(name) { + override fun getFirstOpenCards(): List = getCards() + + override fun canDraw(): Boolean = !isBust() && needToDraw() +} diff --git a/src/main/kotlin/blackjack/domain/Suit.kt b/src/main/kotlin/blackjack/domain/Suit.kt new file mode 100644 index 000000000..82c03c4f8 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Suit.kt @@ -0,0 +1,5 @@ +package blackjack.domain + +enum class Suit { + SPADE, HEART, DIAMOND, CLOVER; +} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt new file mode 100644 index 000000000..2502e9f5f --- /dev/null +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -0,0 +1,25 @@ +package blackjack.view + +object InputView { + private const val PLAYER_NAME_DELIMITER = "," + + fun inputNames(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + val names = readln().split(PLAYER_NAME_DELIMITER).map { it.trim() } + printInterval() + return names + } + + fun inputDrawCommand(name: String): Boolean = runCatching { + println("${name}은(는) 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + + val command = readln().trim().lowercase() + require(command in listOf("y", "n")) { "입력값은 y 또는 n 이어야 합니다. (현재 입력값 : $command)" } + return@runCatching command == "y" + }.getOrElse { + println(it.message) + inputDrawCommand(name) + } + + private fun printInterval() = println() +} diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt new file mode 100644 index 000000000..a73bae9cd --- /dev/null +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -0,0 +1,104 @@ +package blackjack.view + +import blackjack.domain.BlackjackResult +import blackjack.domain.Card +import blackjack.domain.CardNumber +import blackjack.domain.CardResult +import blackjack.domain.Dealer +import blackjack.domain.MatchResult +import blackjack.domain.Participant +import blackjack.domain.Player +import blackjack.domain.Suit + +object OutputView { + private const val SEPARATOR = ", " + + fun printFirstOpenCards(cards: Map>) { + println("${cards.keys.first()}와 ${cards.keys.drop(1).joinToString(SEPARATOR)}에게 2장의 카드를 나누었습니다.") + printCards(cards) + } + + private fun printCards(cards: Map>) { + cards.forEach { (name, cards) -> + println("$name 카드: ${cards.joinToString(SEPARATOR) { it.toText() }}") + } + } + + fun printDrawn(participant: Participant) { + when (participant) { + is Dealer -> println("딜러는 16이하라 한장의 카드를 더 받았습니다.") + is Player -> println( + "${participant.name} 카드: ${participant.getCards().joinToString(SEPARATOR) { it.toText() }}" + ) + } + } + + fun printBlackjackResult(blackJackResult: BlackjackResult) { + printCardResults(blackJackResult.cardResults) + printFinalResult(blackJackResult.matchResults) + } + + private fun printCardResults(cardResults: List) { + printInterval() + cardResults.forEach(::printScore) + } + + private fun printScore(cardResult: CardResult) { + with(cardResult) { + println("${participant.name} 카드: ${cards.joinToString(SEPARATOR) { it.toText() }} - 결과: $scoreSum") + } + } + + private fun printFinalResult(matchResults: List) { + printInterval() + println("## 최종 승패") + printMatchResults(matchResults) + } + + private fun printMatchResults(matchResults: List) { + matchResults.forEach(::printResult) + } + + private fun printResult(matchResult: MatchResult) { + if (matchResult.participant is Dealer) printDealerResult(matchResult) + if (matchResult.participant is Player) printPlayerResult(matchResult) + } + + private fun printDealerResult(result: MatchResult) { + println("딜러: ${result.winCount}승 ${result.drawCount}무 ${result.loseCount}패") + } + + private fun printPlayerResult(matchResult: MatchResult) { + val playerName = matchResult.participant.name + if (matchResult.winCount > 0) println("$playerName: 승") + if (matchResult.drawCount > 0) println("$playerName: 무") + if (matchResult.loseCount > 0) println("$playerName: 패") + } + + private fun Card.toText(): String = "${number.toText()}${suit.toText()}" + + private fun CardNumber.toText(): String = when (this) { + CardNumber.ACE -> "A" + CardNumber.TWO -> "2" + CardNumber.THREE -> "3" + CardNumber.FOUR -> "4" + CardNumber.FIVE -> "5" + CardNumber.SIX -> "6" + CardNumber.SEVEN -> "7" + CardNumber.EIGHT -> "8" + CardNumber.NINE -> "9" + CardNumber.TEN -> "10" + CardNumber.JACK -> "J" + CardNumber.QUEEN -> "Q" + CardNumber.KING -> "K" + } + + private fun Suit.toText(): String = when (this) { + Suit.SPADE -> "스페이드" + Suit.HEART -> "하트" + Suit.DIAMOND -> "다이아몬드" + Suit.CLOVER -> "클로버" + } + + fun printInterval() = println() +} diff --git a/src/test/kotlin/.gitkeep b/src/test/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/test/kotlin/blackjack/domain/CardDeckTest.kt b/src/test/kotlin/blackjack/domain/CardDeckTest.kt new file mode 100644 index 000000000..cd5beb0bd --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardDeckTest.kt @@ -0,0 +1,12 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class CardDeckTest { + @Test + fun `카드를 하나 뽑는다`() { + val cardDeck = CardDeck(Card(CardNumber.KING, Suit.SPADE), Card(CardNumber.EIGHT, Suit.DIAMOND)) + assertThat(cardDeck.draw()).isEqualTo(Card(CardNumber.KING, Suit.SPADE)) + } +} diff --git a/src/test/kotlin/blackjack/domain/CardNumberTest.kt b/src/test/kotlin/blackjack/domain/CardNumberTest.kt new file mode 100644 index 000000000..6baf42214 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardNumberTest.kt @@ -0,0 +1,17 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class CardNumberTest { + @ParameterizedTest(name = "{0} 카드는 {1}점이다.") + @CsvSource( + "ACE, 1", "TWO, 2", "THREE, 3", "FOUR, 4", "FIVE, 5", + "SIX, 6", "SEVEN, 7", "EIGHT, 8", "NINE, 9", "TEN, 10", + "JACK, 10", "QUEEN, 10", "KING, 10" + ) + fun `카드 숫자는 각각의 점수를 반환한다`(cardNumber: CardNumber, expected: Int) { + assertThat(cardNumber.score).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/blackjack/domain/CardTest.kt b/src/test/kotlin/blackjack/domain/CardTest.kt new file mode 100644 index 000000000..63b730253 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardTest.kt @@ -0,0 +1,27 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class CardTest { + @ParameterizedTest(name = "{1} {0} 카드가 존재한다.") + @CsvSource( + "ACE,SPADE", "ACE,HEART", "ACE,DIAMOND", "ACE,CLOVER", + "TWO,SPADE", "TWO,HEART", "TWO,DIAMOND", "TWO,CLOVER", + "THREE,SPADE", "THREE,HEART", "THREE,DIAMOND", "THREE,CLOVER", + "FOUR,SPADE", "FOUR,HEART", "FOUR,DIAMOND", "FOUR,CLOVER", + "FIVE,SPADE", "FIVE,HEART", "FIVE,DIAMOND", "FIVE,CLOVER", + "SIX,SPADE", "SIX,HEART", "SIX,DIAMOND", "SIX,CLOVER", + "SEVEN,SPADE", "SEVEN,HEART", "SEVEN,DIAMOND", "SEVEN,CLOVER", + "EIGHT,SPADE", "EIGHT,HEART", "EIGHT,DIAMOND", "EIGHT,CLOVER", + "NINE,SPADE", "NINE,HEART", "NINE,DIAMOND", "NINE,CLOVER", + "TEN,SPADE", "TEN,HEART", "TEN,DIAMOND", "TEN,CLOVER", + "JACK,SPADE", "JACK,HEART", "JACK,DIAMOND", "JACK,CLOVER", + "QUEEN,SPADE", "QUEEN,HEART", "QUEEN,DIAMOND", "QUEEN,CLOVER", + "KING,SPADE", "KING,HEART", "KING,DIAMOND", "KING,CLOVER" + ) + fun `카드는 각 모양별로 2부터 10, A, J, Q, K가 존재한다`(cardNumber: CardNumber, suit: Suit) { + assertThat(Card.all()).contains(Card(cardNumber, suit)) + } +} diff --git a/src/test/kotlin/blackjack/domain/CardsTest.kt b/src/test/kotlin/blackjack/domain/CardsTest.kt new file mode 100644 index 000000000..81e8086be --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardsTest.kt @@ -0,0 +1,85 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class CardsTest { + @Test + fun `카드 목록에 카드를 한장 추가한다`() { + val cards = Cards() + cards.add(Card(CardNumber.ACE, Suit.SPADE)) + + assertThat(cards.items.size).isEqualTo(1) + } + + @Test + fun `카드 목록에서 첫번째 카드를 반환한다`() { + val cards = Cards( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.THREE, Suit.SPADE), + Card(CardNumber.FIVE, Suit.SPADE) + ) + + assertThat(cards.getFirstCard()).isEqualTo(Card(CardNumber.ACE, Suit.SPADE)) + } + + @Test + fun `A는 기존 총 점수에 11점을 더한 값이 21점 이하이면 11점으로 계산한다`() { + val cards = Cards( + Card(CardNumber.ACE, Suit.SPADE) + ) + + assertThat(cards.calculateTotalScore()).isEqualTo(11) + } + + @Test + fun `A는 기존 총 점수에 11점을 더한 값이 21점을 초과하면 1점으로 계산한다`() { + val cards = Cards( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.ACE, Suit.HEART) + ) + + assertThat(cards.calculateTotalScore()).isEqualTo(12) + } + + @Test + fun `점수가 17점 이상이면 스테이다`() { + val cards = Cards( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.SIX, Suit.SPADE) + ) + + assertThat(cards.isStay()).isTrue + } + + @Test + fun `점수가 17점 미만이면 스테이가 아니다`() { + val cards = Cards( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.FIVE, Suit.SPADE) + ) + + assertThat(cards.isStay()).isFalse + } + + @Test + fun `점수가 21점을 초과하면 버스트다`() { + val cards = Cards( + Card(CardNumber.JACK, Suit.SPADE), + Card(CardNumber.QUEEN, Suit.SPADE), + Card(CardNumber.TWO, Suit.SPADE) + ) + + assertThat(cards.isOverBlackjack()).isTrue + } + + @Test + fun `점수가 21점을 초과하지 않으면 버스트가 아니다`() { + val cards = Cards( + Card(CardNumber.JACK, Suit.SPADE), + Card(CardNumber.QUEEN, Suit.SPADE) + ) + + assertThat(cards.isOverBlackjack()).isFalse + } +} diff --git a/src/test/kotlin/blackjack/domain/DealerTest.kt b/src/test/kotlin/blackjack/domain/DealerTest.kt new file mode 100644 index 000000000..253342c97 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/DealerTest.kt @@ -0,0 +1,94 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class DealerTest { + lateinit var dealer: Dealer + + @BeforeEach + fun setUp() { + dealer = Dealer() + } + + @Test + fun `딜러는 이름을 갖는다`() { + assertThat(dealer.name).isEqualTo("딜러") + } + + @Test + fun `딜러가 처음 공개할 카드는 1장이다`() { + with(dealer) { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + } + + assertThat(dealer.getFirstOpenCards().size).isEqualTo(1) + } + + @Test + fun `딜러는 자신이 처음 공개할 카드를 반환한다`() { + dealer.addCard(Card(CardNumber.ACE, Suit.SPADE)) + dealer.addCard(Card(CardNumber.FIVE, Suit.SPADE)) + + assertThat(dealer.getFirstOpenCards()).isEqualTo(listOf(Card(CardNumber.ACE, Suit.SPADE))) + } + + @Test + fun `딜러는 카드의 합이 16점 이하면 카드를 뽑을 수 있다`() { + dealer.addCard(Card(CardNumber.ACE, Suit.SPADE)) + dealer.addCard(Card(CardNumber.FIVE, Suit.SPADE)) + + assertThat(dealer.canDraw()).isTrue + } + + @Test + fun `딜러는 카드의 합이 17점 이상이면 더 이상 카드를 뽑을 수 없다`() { + dealer.addCard(Card(CardNumber.ACE, Suit.SPADE)) + dealer.addCard(Card(CardNumber.SIX, Suit.SPADE)) + + assertThat(dealer.canDraw()).isFalse + } + + @Test + fun `딜러는 카드 목록에 카드를 추가한다`() { + dealer.addCard(Card(CardNumber.TWO, Suit.SPADE)) + + assertThat(dealer.getCards()).containsExactly(Card(CardNumber.TWO, Suit.SPADE)) + } + + @Test + fun `딜러가 보유한 카드를 반환한다`() { + dealer.addCard(Card(CardNumber.ACE, Suit.SPADE)) + dealer.addCard(Card(CardNumber.JACK, Suit.SPADE)) + + assertThat(dealer.getCards()).isEqualTo( + listOf( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.JACK, Suit.SPADE) + ) + ) + } + + @ParameterizedTest + @CsvSource( + "SPADE, ACE, HEART, ACE, 12", + "SPADE, ACE, SPADE, JACK, 21", + "SPADE, TEN, HEART, SEVEN, 17" + ) + fun `자신의 점수를 반환한다`( + firstCardSuit: Suit, + firstCardNumber: CardNumber, + secondCardSuit: Suit, + secondCardNumber: CardNumber, + expected: Int + ) { + dealer.addCard(Card(firstCardNumber, firstCardSuit)) + dealer.addCard(Card(secondCardNumber, secondCardSuit)) + + assertThat(dealer.getTotalScore()).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/blackjack/domain/ParticipantsTest.kt b/src/test/kotlin/blackjack/domain/ParticipantsTest.kt new file mode 100644 index 000000000..a2251a9a6 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/ParticipantsTest.kt @@ -0,0 +1,93 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ParticipantsTest { + @Test + fun `참여자들은 최소 2명이어야 한다`() { + val players = emptyList() + + Assertions.assertThatIllegalArgumentException().isThrownBy { Participants(players) } + .withMessageContaining("블랙잭은 딜러를 포함하여 최소 2명에서 최대 8명의 플레이어가 참여 가능합니다. (현재 플레이어수 : 0명)") + } + + @Test + fun `참여자들은 최대 8명까지 가능하다`() { + val players = listOf( + Player("a"), Player("b"), Player("c"), + Player("d"), Player("e"), Player("f"), + Player("g"), Player("h"), Player("h") + ) + + Assertions.assertThatIllegalArgumentException().isThrownBy { Participants(players) } + .withMessageContaining("블랙잭은 딜러를 포함하여 최소 2명에서 최대 8명의 플레이어가 참여 가능합니다. (현재 플레이어수 : 9명)") + } + + @Test + fun `참여자들이 카드를 2장씩 뽑는다`() { + val cardDeck = CardDeck( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.THREE, Suit.SPADE), + Card(CardNumber.FIVE, Suit.SPADE), + Card(CardNumber.ACE, Suit.CLOVER), + Card(CardNumber.THREE, Suit.CLOVER), + Card(CardNumber.FIVE, Suit.CLOVER) + ) + val players = listOf(Player("부나"), Player("반달"), Player("글로")) + val participants = Participants(players) + + participants.drawFirst(cardDeck) + + assertAll( + { + assertThat(players[0].getCards()).containsExactly( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.THREE, Suit.SPADE) + ) + }, + { + assertThat(players[1].getCards()).containsExactly( + Card(CardNumber.FIVE, Suit.SPADE), + Card(CardNumber.ACE, Suit.CLOVER) + ) + }, + { + assertThat(players[2].getCards()).containsExactly( + Card(CardNumber.THREE, Suit.CLOVER), + Card(CardNumber.FIVE, Suit.CLOVER) + ) + }, + ) + } + + @ParameterizedTest(name = "{0}가(이) 카드를 뽑을 수 있을 때까지 뽑는다") + @ValueSource(strings = ["부나", "반달", "로피", "글로", "코비", "뽀또"]) + fun `모든 플레이어들이 카드를 뽑을 수 있을 때까지 뽑는다`(name: String) { + val participants = Participants(Dealer(), Player(name)) + var prevCardCount = 0 + + participants.takePlayerTurns(CardDeck()) { player -> + val actual = player.getCards().size + assertThat(++prevCardCount).isEqualTo(actual) + } + } + + @Test + fun `딜러가 카드를 뽑을 수 있을 때까지 뽑는다`() { + val participants = Participants(Dealer(), Player("부나"), Player("반달")) + var prevCardCount = 0 + + participants.takeDealerTurns(CardDeck()) { dealer -> + val actual = dealer.getCards().size + assertThat(++prevCardCount).isEqualTo(actual) + } + } + + private fun Participants(vararg participants: Participant): Participants = + Participants(participants.toList()) +} diff --git a/src/test/kotlin/blackjack/domain/PlayerTest.kt b/src/test/kotlin/blackjack/domain/PlayerTest.kt new file mode 100644 index 000000000..d07e5a68a --- /dev/null +++ b/src/test/kotlin/blackjack/domain/PlayerTest.kt @@ -0,0 +1,188 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class PlayerTest { + lateinit var player: Player + + @BeforeEach + fun setUp() { + player = Player("pobi") + } + + @Test + fun `플레이어는 이름을 갖는다`() { + assertThat(player.name).isEqualTo("pobi") + } + + @Test + fun `플레이어가 처음 공개할 카드는 2장이다`() { + with(player) { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + } + + assertThat(player.getFirstOpenCards().size).isEqualTo(2) + } + + @Test + fun `플레이어는 자신이 처음 공개할 카드를 반환한다`() { + player.addCard(Card(CardNumber.ACE, Suit.SPADE)) + player.addCard(Card(CardNumber.FIVE, Suit.SPADE)) + + assertThat(player.getFirstOpenCards()).isEqualTo( + listOf( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.FIVE, Suit.SPADE) + ) + ) + } + + @Test + fun `카드의 합이 21을 초과하지 않으면 카드를 뽑을 수 있다`() { + player.addCard(Card(CardNumber.ACE, Suit.SPADE)) + player.addCard(Card(CardNumber.KING, Suit.SPADE)) + + assertThat(player.canDraw()).isTrue + } + + @Test + fun `카드의 합이 21을 초과하면 더 이상 카드를 뽑을 수 없다`() { + player.addCard(Card(CardNumber.FOUR, Suit.SPADE)) + player.addCard(Card(CardNumber.EIGHT, Suit.SPADE)) + player.addCard(Card(CardNumber.KING, Suit.SPADE)) + + assertThat(player.canDraw()).isFalse + } + + @Test + fun `플레이어는 카드 목록에 카드를 추가한다`() { + player.addCard(Card(CardNumber.TWO, Suit.SPADE)) + + assertThat(player.getCards()).containsExactly(Card(CardNumber.TWO, Suit.SPADE)) + } + + @Test + fun `플레이어가 보유한 카드를 반환한다`() { + player.addCard(Card(CardNumber.ACE, Suit.SPADE)) + player.addCard(Card(CardNumber.JACK, Suit.SPADE)) + + assertThat(player.getCards()).isEqualTo( + listOf( + Card(CardNumber.ACE, Suit.SPADE), + Card(CardNumber.JACK, Suit.SPADE) + ) + ) + } + + @ParameterizedTest + @CsvSource( + "SPADE, ACE, HEART, ACE, 12", + "SPADE, ACE, SPADE, JACK, 21", + "SPADE, TEN, HEART, SEVEN, 17" + ) + fun `자신의 점수를 반환한다`( + firstCardSuit: Suit, + firstCardNumber: CardNumber, + secondCardSuit: Suit, + secondCardNumber: CardNumber, + expected: Int + ) { + player.addCard(Card(firstCardNumber, firstCardSuit)) + player.addCard(Card(secondCardNumber, secondCardSuit)) + + assertThat(player.getTotalScore()).isEqualTo(expected) + } + + @Test + fun `본인과 상대방이 모두 21점을 초과하면 무승부이다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + addCard(Card(CardNumber.TWO, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.QUEEN, Suit.DIAMOND)) + addCard(Card(CardNumber.THREE, Suit.DIAMOND)) + } + + assertThat(me judge other).isEqualTo(GameResult.DRAW) + } + + @Test + fun `본인만 21점을 초과하면 패배한다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + addCard(Card(CardNumber.TWO, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.QUEEN, Suit.DIAMOND)) + } + + assertThat(me judge other).isEqualTo(GameResult.LOSE) + } + + @Test + fun `상대방만 21점을 초과하면 승리한다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.QUEEN, Suit.DIAMOND)) + addCard(Card(CardNumber.TWO, Suit.SPADE)) + } + + assertThat(me judge other).isEqualTo(GameResult.WIN) + } + + @Test + fun `본인과 상대방이 21점을 초과하지 않고 점수가 동일하면 무승부이다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.QUEEN, Suit.DIAMOND)) + } + + assertThat(me judge other).isEqualTo(GameResult.DRAW) + } + + @Test + fun `본인과 상대방이 21점을 초과하지 않고 본인의 점수가 높으면 승리한다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.QUEEN, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.TWO, Suit.DIAMOND)) + } + + assertThat(me judge other).isEqualTo(GameResult.WIN) + } + + @Test + fun `본인과 상대방이 21점을 초과하지 않고 상대방의 점수가 높으면 패배한다`() { + val me = Player("부나").apply { + addCard(Card(CardNumber.JACK, Suit.SPADE)) + addCard(Card(CardNumber.TWO, Suit.SPADE)) + } + val other = Player("반달").apply { + addCard(Card(CardNumber.JACK, Suit.DIAMOND)) + addCard(Card(CardNumber.QUEEN, Suit.DIAMOND)) + } + + assertThat(me judge other).isEqualTo(GameResult.LOSE) + } +} diff --git a/src/test/kotlin/blackjack/domain/SuitTest.kt b/src/test/kotlin/blackjack/domain/SuitTest.kt new file mode 100644 index 000000000..5510bded3 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/SuitTest.kt @@ -0,0 +1,14 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class SuitTest { + + @ParameterizedTest + @CsvSource("SPADE, SPADE", "HEART, HEART", "DIAMOND, DIAMOND", "CLOVER, CLOVER") + fun `카드 모양에는 스페이드, 하트, 다이아몬드, 클로버가 있다`(suit: Suit, expected: String) { + assertThat(suit.name).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/study/DslTest.kt b/src/test/kotlin/study/DslTest.kt new file mode 100644 index 000000000..784724857 --- /dev/null +++ b/src/test/kotlin/study/DslTest.kt @@ -0,0 +1,133 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class DslTest { + @Test + fun name() { + val person = introduce { + name("부나") + } + assertThat(person.name).isEqualTo("부나") + } + + @Test + fun company() { + val person = introduce { + name("부나") + company("우아한테크코스") + } + assertThat(person.name).isEqualTo("부나") + assertThat(person.company).isEqualTo("우아한테크코스") + } + + @Test + fun skills() { + val person = introduce { + name("부나") + company("우아한테크코스") + skills { + soft("A passion for problem solving") + soft("Good communication skills") + hard("Kotlin") + } + } + assertThat(person.name).isEqualTo("부나") + assertThat(person.company).isEqualTo("우아한테크코스") + assertThat(person.skills["soft"]).containsExactly("A passion for problem solving", "Good communication skills") + assertThat(person.skills["hard"]).containsExactly("Kotlin") + } + + @Test + fun language() { + val person = introduce { + name("부나") + company("우아한테크코스") + skills { + soft("A passion for problem solving") + soft("Good communication skills") + hard("Kotlin") + } + languages { + "Korean" level 5 + "English" level 3 + } + } + assertThat(person.name).isEqualTo("부나") + assertThat(person.company).isEqualTo("우아한테크코스") + assertThat(person.skills["soft"]).containsExactly("A passion for problem solving", "Good communication skills") + assertThat(person.skills["hard"]).containsExactly("Kotlin") + assertThat(person.languages["Korean"]).isEqualTo(5) + assertThat(person.languages["English"]).isEqualTo(3) + } +} + +fun introduce(block: PersonBuilder.() -> Unit): Person { + /*val person = Person() + person.block() + return person*/ + return PersonBuilder().apply(block).build() +} + +class PersonBuilder { + private lateinit var name: String + private var company: String? = null + private val skills: Skills = Skills() + private val languages: Languages = Languages() + + fun name(name: String) { + this.name = name + } + + fun company(company: String) { + this.company = company + } + + fun soft(skill: String) { + skills.add("soft", skill) + } + + fun hard(skill: String) { + skills.add("hard", skill) + } + + fun skills(block: PersonBuilder.() -> Unit) { + block() + } + + infix fun String.level(level: Int) { + languages.level(this, level) + } + + fun languages(block: PersonBuilder.() -> Unit) { + block() + } + + fun build(): Person = Person(name, company, skills, languages) +} + +data class Person( + val name: String, + val company: String?, + val skills: Skills, + val languages: Languages, +) + +class Languages { + private val languages: MutableMap = mutableMapOf() + + fun level(language: String, level: Int) { + languages[language] = level + } + + operator fun get(key: String): Int = languages[key] ?: 0 +} + +class Skills { + private val skills: MutableMap> = mutableMapOf() + + fun add(type: String, skill: String) { + skills.getOrPut(type, ::mutableListOf).add(skill) + } + + operator fun get(key: String): List = skills[key]?.toList() ?: listOf() +}