diff --git a/README.md b/README.md index e1c7c927d8..d7a8ea8847 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack + +## step2 - 블랙잭 + +기능 요구사항 +- 딜러 + - [x] : 게임 시작 시에, Player 에게 카드를 2장씩 나눠준다. + - [x] : Player 에게 카드를 나눠준다. + - [x] : Player 에게 카드를 나눠줄 수 있는지 확인한다. +- 플레이어 + - [x] : 딜러에게 카드를 달라고 한다. + - [x] : 받은 카드의 총합을 계산한다. +- 게임 + - [x] : N 명의 플레이어가 play 할 수 있다. + +## 용어 정리 +- Hand : 플레이어가 들고 있는 패 +- Card : 카드 (단수) +- Rank : 무늬별로 A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K의 13 가지 끗수(rank) +- Suit : 스페이드, 하트, 다이아몬드, 클럽의 4 가지 무늬(suit) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e78e729567..46d50fa69d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,8 @@ repositories { dependencies { testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2") testImplementation("org.assertj", "assertj-core", "3.22.0") - testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3") + testImplementation("io.kotest", "kotest-runner-junit5", "5.7.2") + testImplementation("io.kotest", "kotest-assertions-core-jvm", "5.7.2") } tasks { diff --git a/src/main/kotlin/blackJack/BlackJackRunner.kt b/src/main/kotlin/blackJack/BlackJackRunner.kt new file mode 100644 index 0000000000..ac4f83d1f4 --- /dev/null +++ b/src/main/kotlin/blackJack/BlackJackRunner.kt @@ -0,0 +1,9 @@ +package blackJack + +import blackJack.controller.BlackJackController + +class BlackJackRunner + +fun main() { + BlackJackController().play() +} diff --git a/src/main/kotlin/blackJack/controller/BlackJackController.kt b/src/main/kotlin/blackJack/controller/BlackJackController.kt new file mode 100644 index 0000000000..9c5e1dab9d --- /dev/null +++ b/src/main/kotlin/blackJack/controller/BlackJackController.kt @@ -0,0 +1,41 @@ +package blackJack.controller + +import blackJack.model.Dealer +import blackJack.model.Player +import blackJack.model.askMoreCard +import blackJack.model.checkDrawCardIsAllowedFor +import blackJack.view.InputView +import blackJack.view.OutputView + +class BlackJackController { + fun play() { + val req = InputView.getNames() + + val candidates = req.map { Player(it) } + val dealer = Dealer("dealer") + + val players = dealer.startGame(candidates) + OutputView.printPlayersState(players) + + players.forEach { player -> + shouldContinue(player, dealer) + } + + OutputView.printFinalState(players) + } + + private fun shouldContinue(player: Player, dealer: Dealer) { + while (true) { + val req = InputView.getPlayerInput(player.name) + if (req == "n") { + break + } + player askMoreCard dealer + + if ((dealer checkDrawCardIsAllowedFor player).not()) { + break + } + OutputView.printPlayerState(player) + } + } +} diff --git a/src/main/kotlin/blackJack/model/Card.kt b/src/main/kotlin/blackJack/model/Card.kt new file mode 100644 index 0000000000..f457d73136 --- /dev/null +++ b/src/main/kotlin/blackJack/model/Card.kt @@ -0,0 +1,9 @@ +package blackJack.model + +import blackJack.model.enums.Rank +import blackJack.model.enums.Suit + +data class Card( + val suit: Suit, + val rank: Rank +) diff --git a/src/main/kotlin/blackJack/model/CardDeck.kt b/src/main/kotlin/blackJack/model/CardDeck.kt new file mode 100644 index 0000000000..6543781e15 --- /dev/null +++ b/src/main/kotlin/blackJack/model/CardDeck.kt @@ -0,0 +1,25 @@ +package blackJack.model + +import blackJack.model.enums.Rank +import blackJack.model.enums.Suit + +class CardDeck(val cards: List) { + companion object { + fun of(): CardDeck { + val cards = generateAllCards() + return CardDeck(cards) + } + + private fun generateAllCards(): List { + return Suit.values().flatMap { suit -> + generateCardsForSuit(suit) + } + } + + private fun generateCardsForSuit(suit: Suit): List { + return Rank.values().map { rank -> + Card(suit, rank) + } + } + } +} diff --git a/src/main/kotlin/blackJack/model/Dealer.kt b/src/main/kotlin/blackJack/model/Dealer.kt new file mode 100644 index 0000000000..7ef62379b4 --- /dev/null +++ b/src/main/kotlin/blackJack/model/Dealer.kt @@ -0,0 +1,39 @@ +package blackJack.model + +class Dealer(val name: String) { + private var cardDeck = CardDeck.of() + val MAXIMUM_SCORE = 21 + + fun countCard(): Int { + return cardDeck.cards.size + } + + fun drawCard(): Card { + val currentCard = cardDeck.cards + .shuffled() + .first() + + cardDeck = cardDeck.cards + .filter { it != currentCard } + .let(::CardDeck) + + return currentCard + } + + fun startGame(players: List): List { + return initializePlayerHands(players) + } + + private fun initializePlayerHands(players: List): List { + players.forEach { player -> + player requestCardToDealer drawCard() + player requestCardToDealer drawCard() + } + + return players + } +} + +infix fun Dealer.checkDrawCardIsAllowedFor(player: Player): Boolean { + return player.calculateScore() < MAXIMUM_SCORE +} diff --git a/src/main/kotlin/blackJack/model/Player.kt b/src/main/kotlin/blackJack/model/Player.kt new file mode 100644 index 0000000000..237b964d5e --- /dev/null +++ b/src/main/kotlin/blackJack/model/Player.kt @@ -0,0 +1,20 @@ +package blackJack.model + +class Player( + val name: String, + var hand: List = listOf() +) { + fun calculateScore(): Int { + return hand.sumOf { it.rank.score } + } +} + +infix fun Player.askMoreCard(dealer: Dealer) { + if (dealer checkDrawCardIsAllowedFor this) { + this requestCardToDealer dealer.drawCard() + } +} + +infix fun Player.requestCardToDealer(card: Card) { + hand += card +} diff --git a/src/main/kotlin/blackJack/model/enums/Rank.kt b/src/main/kotlin/blackJack/model/enums/Rank.kt new file mode 100644 index 0000000000..b538499735 --- /dev/null +++ b/src/main/kotlin/blackJack/model/enums/Rank.kt @@ -0,0 +1,21 @@ +package blackJack.model.enums + +enum class Rank( + val symbol: String, + val score: Int, + val isAce: Boolean = false +) { + ACE("A", 1, true), + TWO("2", 2), + THREE("3", 3), + FOUR("4", 4), + FIVE("5", 5), + SIX("6", 6), + SEVEN("7", 7), + EIGHT("8", 8), + NINE("9", 9), + TEN("10", 10), + JACK("J", 10), + QUEEN("Q", 10), + KING("K", 10), +} diff --git a/src/main/kotlin/blackJack/model/enums/Suit.kt b/src/main/kotlin/blackJack/model/enums/Suit.kt new file mode 100644 index 0000000000..2f07838ac4 --- /dev/null +++ b/src/main/kotlin/blackJack/model/enums/Suit.kt @@ -0,0 +1,8 @@ +package blackJack.model.enums + +enum class Suit(val symbol: String) { + CLUBS("클로버"), + DIAMONDS("다이아몬드"), + HEARTS("하트"), + SPADES("스페이드") +} diff --git a/src/main/kotlin/blackJack/view/InputView.kt b/src/main/kotlin/blackJack/view/InputView.kt new file mode 100644 index 0000000000..f58f356a9c --- /dev/null +++ b/src/main/kotlin/blackJack/view/InputView.kt @@ -0,0 +1,22 @@ +package blackJack.view + +object InputView { + private const val PLAYER_QUERY_FORMAT = "%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)" + + fun getNames(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + val input = readlnOrNull() ?: throw IllegalArgumentException("콘솔 입력을 확인해 주세요.") + + return input.replace(" ", "") + .split(",") + } + + fun getPlayerInput(playerName: String): String { + println(PLAYER_QUERY_FORMAT.format(playerName)) + + val input = readlnOrNull() ?: throw IllegalArgumentException("콘솔 입력을 확인해 주세요.") + require(input == "y" || input == "n") ?: throw IllegalArgumentException("y 또는 n을 입력해 주세요.") + + return input + } +} \ No newline at end of file diff --git a/src/main/kotlin/blackJack/view/OutputView.kt b/src/main/kotlin/blackJack/view/OutputView.kt new file mode 100644 index 0000000000..28f4d595b2 --- /dev/null +++ b/src/main/kotlin/blackJack/view/OutputView.kt @@ -0,0 +1,28 @@ +package blackJack.view + +import blackJack.model.Player + +object OutputView { + private const val PLAYER_STATE_FORMAT = "%s카드 : %s" + private const val PLAYER__FINAL_STATE_FORMAT = "%s카드 : %s - 결과: %s" + + fun printPlayersState(players: List) { + players.forEach { printPlayerState(it) } + } + + fun printFinalState(players: List) { + players.forEach { printPlayerFinalState(it) } + } + + fun printPlayerState(player: Player) { + player.hand + .joinToString { it.rank.symbol + it.suit.symbol } + .let { println(PLAYER_STATE_FORMAT.format(player.name, it)) } + } + + private fun printPlayerFinalState(player: Player) { + player.hand + .joinToString { it.rank.symbol + it.suit.symbol } + .let { println(PLAYER__FINAL_STATE_FORMAT.format(player.name, it, player.calculateScore())) } + } +} diff --git a/src/main/kotlin/study/builder/LangaugeBuilder.kt b/src/main/kotlin/study/builder/LangaugeBuilder.kt index de31c6a614..4af95888bd 100644 --- a/src/main/kotlin/study/builder/LangaugeBuilder.kt +++ b/src/main/kotlin/study/builder/LangaugeBuilder.kt @@ -1,6 +1,6 @@ package study.builder -import study.dto.Language +import study.domain.Language class LangaugeBuilder { private var languages: List = emptyList() diff --git a/src/main/kotlin/study/builder/PersonBuilder.kt b/src/main/kotlin/study/builder/PersonBuilder.kt index 9e91c6d38a..c8b9541917 100644 --- a/src/main/kotlin/study/builder/PersonBuilder.kt +++ b/src/main/kotlin/study/builder/PersonBuilder.kt @@ -1,14 +1,14 @@ package study.builder -import study.dto.Language -import study.dto.Person -import study.dto.Skill +import study.domain.Language +import study.domain.Person +import study.domain.Skill class PersonBuilder { - private lateinit var name: String - private lateinit var company: String - private lateinit var skills: List - private lateinit var languages: List + private var name: String = "홍길동" + private var company: String = "미정" + private var skills: List = emptyList() + private var languages: List = emptyList() fun name(value: String) { name = value diff --git a/src/main/kotlin/study/builder/SkillsBuilder.kt b/src/main/kotlin/study/builder/SkillsBuilder.kt index 29dc790e26..8e01d9c764 100644 --- a/src/main/kotlin/study/builder/SkillsBuilder.kt +++ b/src/main/kotlin/study/builder/SkillsBuilder.kt @@ -1,6 +1,6 @@ package study.builder -import study.dto.Skill +import study.domain.Skill class SkillsBuilder { private var skills: List = emptyList() diff --git a/src/main/kotlin/study/dto/Language.kt b/src/main/kotlin/study/domain/Language.kt similarity index 72% rename from src/main/kotlin/study/dto/Language.kt rename to src/main/kotlin/study/domain/Language.kt index fc895d3e47..ff1f571ecc 100644 --- a/src/main/kotlin/study/dto/Language.kt +++ b/src/main/kotlin/study/domain/Language.kt @@ -1,3 +1,3 @@ -package study.dto +package study.domain data class Language(val name: String, val level: Int) diff --git a/src/main/kotlin/study/dto/Person.kt b/src/main/kotlin/study/domain/Person.kt similarity index 84% rename from src/main/kotlin/study/dto/Person.kt rename to src/main/kotlin/study/domain/Person.kt index f0c3e62f51..caab2c25f9 100644 --- a/src/main/kotlin/study/dto/Person.kt +++ b/src/main/kotlin/study/domain/Person.kt @@ -1,3 +1,3 @@ -package study.dto +package study.domain data class Person(val name: String, val company: String, val skills: List, val languages: List) diff --git a/src/main/kotlin/study/dto/Skill.kt b/src/main/kotlin/study/domain/Skill.kt similarity index 67% rename from src/main/kotlin/study/dto/Skill.kt rename to src/main/kotlin/study/domain/Skill.kt index efacd5dcd9..723ba6c7ea 100644 --- a/src/main/kotlin/study/dto/Skill.kt +++ b/src/main/kotlin/study/domain/Skill.kt @@ -1,3 +1,3 @@ -package study.dto +package study.domain data class Skill(val description: String) diff --git a/src/main/kotlin/study/dsl/Person.kt b/src/main/kotlin/study/dsl/Person.kt index c2df507322..987ed559cd 100644 --- a/src/main/kotlin/study/dsl/Person.kt +++ b/src/main/kotlin/study/dsl/Person.kt @@ -3,9 +3,9 @@ package study.dsl import study.builder.LangaugeBuilder import study.builder.PersonBuilder import study.builder.SkillsBuilder -import study.dto.Language -import study.dto.Person -import study.dto.Skill +import study.domain.Language +import study.domain.Person +import study.domain.Skill fun introduce(block: PersonBuilder.() -> Unit): Person { return PersonBuilder().apply(block).build() diff --git a/src/test/kotlin/blackjack/domain/DealerSpec.kt b/src/test/kotlin/blackjack/domain/DealerSpec.kt new file mode 100644 index 0000000000..6efb615800 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/DealerSpec.kt @@ -0,0 +1,48 @@ +package blackjack.domain + +import blackJack.model.Dealer +import blackJack.model.Player +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class DealerSpec : BehaviorSpec({ + given("딜러와 플레이어 2명이 있을떄") { + val dealer = Dealer("dealer") + val player1 = Player("player1") + val player2 = Player("player2") + val players = listOf(player1, player2) + + `when`("게임을 시작했을때") { + dealer.startGame(players) + + then("플레이어는 딜러가 나눠준 카드를 두장씩 갖고 있다.") { + players[0].hand.size shouldBe 2 + players[1].hand.size shouldBe 2 + } + } + } + + given("딜러에게 카드덱이 주어지고") { + val dealer = Dealer("dealer") + + `when`("딜러가 게임을 시작했을때") { + val players = listOf(Player("player1"), Player("player2")) + dealer.startGame(players) + + then("플레이어는 딜러가 나눠준 카드를 두장씩 갖고 있다.") { + players[0].hand.size shouldBe 2 + players[1].hand.size shouldBe 2 + } + } + + `when`("딜러에게 카드를 한장 나눠줬을때") { + val prevCount = dealer.countCard() + dealer.drawCard() + val currentCount = dealer.countCard() + + then("카드덱에는 한장의 카드가 사라졌다.") { + currentCount shouldBe prevCount - 1 + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/PlayerSpec.kt b/src/test/kotlin/blackjack/domain/PlayerSpec.kt new file mode 100644 index 0000000000..d29f7d8f35 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/PlayerSpec.kt @@ -0,0 +1,57 @@ +package blackjack.domain + +import blackJack.model.Card +import blackJack.model.Dealer +import blackJack.model.Player +import blackJack.model.askMoreCard +import blackJack.model.enums.Rank +import blackJack.model.enums.Suit +import blackJack.model.requestCardToDealer +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class PlayerSpec : BehaviorSpec({ + + given("플레이어가 2장의 카드를 받았을 때") { + val player = Player("플레이어") + val dealer = Dealer("딜러") + player requestCardToDealer Card(Suit.SPADES, Rank.ACE) + player requestCardToDealer Card(Suit.CLUBS, Rank.EIGHT) + + `when`("플레이어가 가진 카드의 합을 구하면") { + val score = player.calculateScore() + + then("플레이어의 카드 합은 9이다.") { + score shouldBe 9 + } + } + + `when`("bust 상태가 아닌 플레이어가 딜러에게 카드를 더 달라고 요구하면") { + val currentCardCount = player.hand.size + player askMoreCard dealer + val newCardCount = player.hand.size + + then("플레이어는 카드를 한장 더 받는다.") { + currentCardCount shouldBe newCardCount - 1 + } + } + } + + given("bust 상태인 플레이어가") { + val player = Player("플레이어") + val dealer = Dealer("딜러") + player requestCardToDealer Card(Suit.SPADES, Rank.JACK) + player requestCardToDealer Card(Suit.SPADES, Rank.QUEEN) + player requestCardToDealer Card(Suit.CLUBS, Rank.EIGHT) + + `when`("딜러에게 카드를 더 달라고 요구하면") { + val currentCardCount = player.hand.size + player askMoreCard dealer + val newCardCount = player.hand.size + + then("플레이어는 카드를 받지 않는다.") { + newCardCount shouldBe currentCardCount + } + } + } +}) diff --git a/src/test/kotlin/study/DslTest.kt b/src/test/kotlin/study/DslTest.kt index c49c9dc5be..3ca9f8b3e1 100644 --- a/src/test/kotlin/study/DslTest.kt +++ b/src/test/kotlin/study/DslTest.kt @@ -18,6 +18,27 @@ class DslTest { skills[0].description shouldBe "A passion for problem solving" } + @Test + fun name() { + val person = introduce {} + person.name shouldBe "홍길동" + } + + @Test + fun skills() { + val person = introduce { + name("홍길동") + company("활빈당") + skills { + soft("A passion for problem solving") + soft("Good communication skills") + hard("Kotlin") + } + } + person.skills[0].description shouldBe "A passion for problem solving" + person.skills.size shouldBe 3 + } + @Test fun languageTest() { val person = introduce {