diff --git a/README.md b/README.md index e1c7c927d8..b3aa3cd3aa 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack + +[x] 숫자와 문양을 가진 카드 구현 + +[x] 카드 뭉치인 덱 구현 + +[x] 점수 합계를 계산할 수 있는 카드목록 구현 + +[x] 이름과 카드 목록을 가질 수 있는 플레이어 구현 + +[x] 카드를 두 장 받는 기능 구현 + +[x] 카드를 한 장 더 받는 기능 구현 + +[x] 21이 초과했는지 판별하는 기능 구현 + +[x] 카드를 그만 받는 기능 구현 + +[x] 입출력 기능 구현 diff --git a/src/main/kotlin/blackjack/Main.kt b/src/main/kotlin/blackjack/Main.kt new file mode 100644 index 0000000000..b49e33d7f8 --- /dev/null +++ b/src/main/kotlin/blackjack/Main.kt @@ -0,0 +1,41 @@ +package blackjack + +import blackjack.domain.BlackJack +import blackjack.domain.Deck +import blackjack.domain.OnGoingPlayer +import blackjack.domain.Player +import blackjack.view.InputView +import blackjack.view.OutputView + +fun main() { + val players = InputView.getPlayers() + + val blackJack = BlackJack(Deck()) + + val finishedPlayers: List = blackJack.play(players) + .onEach { OutputView.printCards(it) } + .map { player -> + var onGoingPlayer = player + + while (true) { + if (onGoingPlayer !is OnGoingPlayer) { + break + } + + val isHit = InputView.isHit(onGoingPlayer.name) + + if (!isHit) { + break + } + + onGoingPlayer = blackJack.hit(onGoingPlayer) + OutputView.printCards(onGoingPlayer) + } + + onGoingPlayer + } + + finishedPlayers.forEach { + OutputView.printResult(it) + } +} diff --git a/src/main/kotlin/blackjack/domain/BlackJack.kt b/src/main/kotlin/blackjack/domain/BlackJack.kt new file mode 100644 index 0000000000..2d56c40304 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackJack.kt @@ -0,0 +1,32 @@ +package blackjack.domain + +class BlackJack( + private val deck: Deck, +) { + fun play(players: List): List { + return players.map { player -> + val drawnCards = Cards( + listOf( + deck.draw(), + deck.draw(), + ) + ) + + OnGoingPlayer.of(player.name, drawnCards) + } + } + + fun hit(player: OnGoingPlayer): Player { + val drawnCard = deck.draw() + + return OnGoingPlayer.of(player.name, player.cards + drawnCard) + } + + fun stay(player: Player): FinishedPlayer { + return FinishedPlayer(player) + } + + companion object { + const val BlackJackedNumber = 21 + } +} diff --git a/src/main/kotlin/blackjack/domain/BlackJackedPlayer.kt b/src/main/kotlin/blackjack/domain/BlackJackedPlayer.kt new file mode 100644 index 0000000000..16392a1c71 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackJackedPlayer.kt @@ -0,0 +1,6 @@ +package blackjack.domain + +class BlackJackedPlayer( + override val name: String, + override val cards: Cards, +) : Player diff --git a/src/main/kotlin/blackjack/domain/BustedPlayer.kt b/src/main/kotlin/blackjack/domain/BustedPlayer.kt new file mode 100644 index 0000000000..c0e700045b --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BustedPlayer.kt @@ -0,0 +1,6 @@ +package blackjack.domain + +class BustedPlayer( + override val name: String, + override val cards: Cards, +) : Player diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt new file mode 100644 index 0000000000..c0d869222c --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Card.kt @@ -0,0 +1,6 @@ +package blackjack.domain + +data class Card( + val suit: Suit, + val rank: Rank, +) diff --git a/src/main/kotlin/blackjack/domain/CardPointStrategy.kt b/src/main/kotlin/blackjack/domain/CardPointStrategy.kt new file mode 100644 index 0000000000..f1bbeed8fb --- /dev/null +++ b/src/main/kotlin/blackjack/domain/CardPointStrategy.kt @@ -0,0 +1,5 @@ +package blackjack.domain + +fun interface CardPointStrategy { + fun getPoint(rank: Rank): Int +} diff --git a/src/main/kotlin/blackjack/domain/Cards.kt b/src/main/kotlin/blackjack/domain/Cards.kt new file mode 100644 index 0000000000..61bd802483 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Cards.kt @@ -0,0 +1,20 @@ +package blackjack.domain + +@JvmInline +value class Cards(val value: List) { + fun getPoints(): Int { + val hardAcePoint = value.sumOf { card -> + HardAcePointStrategy().getPoint(card.rank) + } + + val softAcePointStrategy = value.sumOf { card -> + SoftAcePointStrategy().getPoint(card.rank) + } + + return softAcePointStrategy.takeIf { it <= BlackJack.BlackJackedNumber } ?: hardAcePoint + } + + operator fun plus(other: Card): Cards { + return Cards(value + other) + } +} diff --git a/src/main/kotlin/blackjack/domain/Deck.kt b/src/main/kotlin/blackjack/domain/Deck.kt new file mode 100644 index 0000000000..37e42fe287 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Deck.kt @@ -0,0 +1,23 @@ +package blackjack.domain + +class Deck { + private val cards = ArrayList(setupDeck()) + + fun draw(): Card { + check(cards.isNotEmpty()) { + "덱에 남아있는 카드가 없습니다." + } + + return cards.removeFirst() + } + + private fun setupDeck(): List { + return Suit.values() + .flatMap { suit -> + Rank.values().map { rank -> + Card(suit, rank) + } + } + .shuffled() + } +} diff --git a/src/main/kotlin/blackjack/domain/FinishedPlayer.kt b/src/main/kotlin/blackjack/domain/FinishedPlayer.kt new file mode 100644 index 0000000000..7571d395c4 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/FinishedPlayer.kt @@ -0,0 +1,3 @@ +package blackjack.domain + +class FinishedPlayer(player: Player) : Player by player diff --git a/src/main/kotlin/blackjack/domain/HardAcePointStrategy.kt b/src/main/kotlin/blackjack/domain/HardAcePointStrategy.kt new file mode 100644 index 0000000000..3e7dd12021 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/HardAcePointStrategy.kt @@ -0,0 +1,7 @@ +package blackjack.domain + +class HardAcePointStrategy : CardPointStrategy { + override fun getPoint(rank: Rank): Int { + return rank.value + } +} diff --git a/src/main/kotlin/blackjack/domain/OnGoingPlayer.kt b/src/main/kotlin/blackjack/domain/OnGoingPlayer.kt new file mode 100644 index 0000000000..d911dcc69c --- /dev/null +++ b/src/main/kotlin/blackjack/domain/OnGoingPlayer.kt @@ -0,0 +1,20 @@ +package blackjack.domain + +class OnGoingPlayer( + override val name: String, + override val cards: Cards, +) : Player { + companion object { + fun of(name: String, cards: Cards): Player { + val hardAcePoint = cards.getPoints() + + return if (hardAcePoint == BlackJack.BlackJackedNumber) { + BlackJackedPlayer(name, cards) + } else if (hardAcePoint > BlackJack.BlackJackedNumber) { + BustedPlayer(name, cards) + } else { + OnGoingPlayer(name, cards) + } + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt new file mode 100644 index 0000000000..e1a8b50e0c --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -0,0 +1,6 @@ +package blackjack.domain + +interface Player { + val name: String + val cards: Cards +} diff --git a/src/main/kotlin/blackjack/domain/PreparedPlayer.kt b/src/main/kotlin/blackjack/domain/PreparedPlayer.kt new file mode 100644 index 0000000000..8687057141 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/PreparedPlayer.kt @@ -0,0 +1,7 @@ +package blackjack.domain + +class PreparedPlayer( + override val name: String, +) : Player { + override val cards: Cards = Cards(emptyList()) +} diff --git a/src/main/kotlin/blackjack/domain/Rank.kt b/src/main/kotlin/blackjack/domain/Rank.kt new file mode 100644 index 0000000000..ec505d78a5 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Rank.kt @@ -0,0 +1,17 @@ +package blackjack.domain + +enum class Rank(val value: Int, val nameValue: String) { + Ace(1, "1"), + 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(10, "J"), + Queen(10, "Q"), + King(10, "K"), +} diff --git a/src/main/kotlin/blackjack/domain/SoftAcePointStrategy.kt b/src/main/kotlin/blackjack/domain/SoftAcePointStrategy.kt new file mode 100644 index 0000000000..8f55b75692 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/SoftAcePointStrategy.kt @@ -0,0 +1,10 @@ +package blackjack.domain + +class SoftAcePointStrategy : CardPointStrategy { + override fun getPoint(rank: Rank): Int { + return when(rank) { + Rank.Ace -> 11 + else -> rank.value + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Suit.kt b/src/main/kotlin/blackjack/domain/Suit.kt new file mode 100644 index 0000000000..b83a68219b --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Suit.kt @@ -0,0 +1,8 @@ +package blackjack.domain + +enum class Suit(val value: String) { + Spade("스페이드"), + Club("클로버"), + Diamond("다이아몬드"), + Heart("하트"), +} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt new file mode 100644 index 0000000000..3fecd05944 --- /dev/null +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -0,0 +1,22 @@ +package blackjack.view + +import blackjack.domain.PreparedPlayer + +object InputView { + fun getPlayers(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + + return readln().split(",") + .map { PreparedPlayer(it) } + } + + fun isHit(name: String): Boolean { + println("${name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + + return when (readln()) { + "y" -> true + "n" -> false + else -> error("y/n 으로만 입력해주세요") + } + } +} diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt new file mode 100644 index 0000000000..e440642cba --- /dev/null +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -0,0 +1,17 @@ +package blackjack.view + +import blackjack.domain.Player + +object OutputView { + fun printCards(player: Player) { + println("${player.name}카드: ${player.cards.value.joinToString { "${it.rank.nameValue}${it.suit.value}" }}") + } + + fun printResult(player: Player) { + println( + "${player.name}카드: ${player.cards.value.joinToString { "${it.rank.nameValue}${it.suit.value}" }} - 결과: ${ + player.cards.getPoints() + }" + ) + } +} diff --git a/src/test/kotlin/blackjack/domain/BlackJackTest.kt b/src/test/kotlin/blackjack/domain/BlackJackTest.kt new file mode 100644 index 0000000000..f0dd2e8398 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/BlackJackTest.kt @@ -0,0 +1,80 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class BlackJackTest { + @Test + fun `PreparedPlayer는 카드 두 장을 받은 OngoingPlayer가 된다`() { + val deck = Deck() + val blackJack = BlackJack(deck) + + val preparedPlayers = listOf(PreparedPlayer("a")) + val onGoingPlayers = blackJack.play(preparedPlayers) + + val actualCardsCount = onGoingPlayers.first().cards.value.size + val expectedCardsCount = 2 + + assertThat(actualCardsCount).isEqualTo(expectedCardsCount) + } + + @Test + fun `PreparedPlayer는 OngoingPlayer가 되더라도 이름이 유지된다`() { + val deck = Deck() + val blackJack = BlackJack(deck) + + val preparedPlayers = listOf(PreparedPlayer("a")) + val onGoingPlayers = blackJack.play(preparedPlayers) + + val actualPlayerName = onGoingPlayers.first().name + val expectedPlayerName = "a" + + assertThat(actualPlayerName).isEqualTo(expectedPlayerName) + } + + @Test + fun `진행중인 플레이어는 카드를 더 뽑을 수 있다`() { + val deck = Deck() + val blackJack = BlackJack(deck) + val player = OnGoingPlayer("a", Cards(listOf())) + + val onGoingPlayer = blackJack.hit(player) + + val actualCardsCount = onGoingPlayer.cards.value.size + val expectedCardsCount = 1 + + assertThat(actualCardsCount).isEqualTo(expectedCardsCount) + } + + @Test + fun `카드를 뽑았을 때 21을 초과하면 버스트된 플레이어가 된다`() { + val deck = Deck() + val blackJack = BlackJack(deck) + val cards = Cards( + listOf( + Card(Suit.Spade, Rank.Ten), + Card(Suit.Spade, Rank.Ten), + Card(Suit.Spade, Rank.Ace), + ) + ) + val player = OnGoingPlayer("a", cards) + + val bustedPlayer = blackJack.hit(player) + + assertThat(bustedPlayer).isInstanceOf(BustedPlayer::class.java) + } + + @Test + fun `스테이 하면 종료한 플레이어가 된다`() { + val deck = Deck() + val blackJack = BlackJack(deck) + val player = object : Player { + override val name: String = "" + override val cards: Cards = Cards(emptyList()) + } + + val finishedPlayer = blackJack.stay(player) + + assertThat(finishedPlayer).isInstanceOf(FinishedPlayer::class.java) + } +} diff --git a/src/test/kotlin/blackjack/domain/CardsTest.kt b/src/test/kotlin/blackjack/domain/CardsTest.kt new file mode 100644 index 0000000000..269cb09986 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/CardsTest.kt @@ -0,0 +1,35 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class CardsTest { + @Test + fun `Ace는 카드의 총 합이 21을 넘지 않으면 11으로 활용한다`() { + val cards = Cards( + listOf( + Card(Suit.Spade, Rank.Ace), + Card(Suit.Spade, Rank.Two), + ) + ) + + val totalPoint = cards.getPoints() + + assertThat(totalPoint).isEqualTo(13) + } + + @Test + fun `Ace를 11로 사용해서 카드의 총 합이 21을 넘는 경우 1로 활용한다`() { + val cards = Cards( + listOf( + Card(Suit.Spade, Rank.Ace), + Card(Suit.Spade, Rank.Two), + Card(Suit.Spade, Rank.Ten), + ) + ) + + val totalPoint = cards.getPoints() + + assertThat(totalPoint).isEqualTo(13) + } +} diff --git a/src/test/kotlin/blackjack/domain/DeckTest.kt b/src/test/kotlin/blackjack/domain/DeckTest.kt new file mode 100644 index 0000000000..23a901c902 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/DeckTest.kt @@ -0,0 +1,29 @@ +package blackjack.domain + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class DeckTest { + @Test + fun `Deck은 52장의 카드를 뽑을 수 있다`() { + val deck = Deck() + + assertDoesNotThrow { + repeat(52) { + deck.draw() + } + } + } + + @Test + fun `Deck은 52장 초과로 카드를 뽑을 수 없다`() { + val deck = Deck() + + assertThrows { + repeat(53) { + deck.draw() + } + } + } +} diff --git a/src/test/kotlin/blackjack/domain/HardAcePointStrategyTest.kt b/src/test/kotlin/blackjack/domain/HardAcePointStrategyTest.kt new file mode 100644 index 0000000000..e911b5446b --- /dev/null +++ b/src/test/kotlin/blackjack/domain/HardAcePointStrategyTest.kt @@ -0,0 +1,18 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +class HardAcePointStrategyTest { + @ParameterizedTest + @EnumSource(value = Rank::class) + fun `모든 카드는 원래의 점수로 계산한다`(rank: Rank) { + val hardAcePointStrategy = HardAcePointStrategy() + + val actualPoint = hardAcePointStrategy.getPoint(rank) + val expectedPoint = rank.value + + Assertions.assertThat(actualPoint).isEqualTo(expectedPoint) + } +} diff --git a/src/test/kotlin/blackjack/domain/SoftAcePointStrategyTest.kt b/src/test/kotlin/blackjack/domain/SoftAcePointStrategyTest.kt new file mode 100644 index 0000000000..abaf7cb320 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/SoftAcePointStrategyTest.kt @@ -0,0 +1,29 @@ +package blackjack.domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +class SoftAcePointStrategyTest { + @Test + fun `Ace는 11점으로 계산한다`() { + val softAcePointStrategy = SoftAcePointStrategy() + + val actualPoint = softAcePointStrategy.getPoint(Rank.Ace) + val expectedPoint = 11 + + assertThat(actualPoint).isEqualTo(expectedPoint) + } + + @ParameterizedTest + @EnumSource(value = Rank::class, names = ["Ace"], mode = EnumSource.Mode.EXCLUDE) + fun `Ace가 아니면 원래의 점수로 계산한다`(rank: Rank) { + val softAcePointStrategy = SoftAcePointStrategy() + + val actualPoint = softAcePointStrategy.getPoint(rank) + val expectedPoint = rank.value + + assertThat(actualPoint).isEqualTo(expectedPoint) + } +}