diff --git a/README.md b/README.md index e1c7c927d8..5e0184a9a7 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack + +게임 관련 기능 + +- [x] 카드 정보 클래스 구성 +- [x] 참여자 클래스 구성 +- [x] 기본 카드 나눠주기(게임 시작과 함께 2장씩 나눠준다.) +- [x] 카드를 나눠줄 수 있는 상태 확인(나눠줄 카드가 남아있는지, 현재 점수가 21점보다 작은지 판단) +- [x] y/n 응답 별 카드 배분 +- [x] 최적의 점수 구하기 + +입출력 기능 + +- [x] 참여자 입력 구현 +- [x] 현재 카드 상태 출력 +- [x] y/n 응답 묻기 +- [x] 결과 출력 \ No newline at end of file diff --git a/src/main/kotlin/blackjack/controller/BlackJackGame.kt b/src/main/kotlin/blackjack/controller/BlackJackGame.kt new file mode 100644 index 0000000000..e1cb314974 --- /dev/null +++ b/src/main/kotlin/blackjack/controller/BlackJackGame.kt @@ -0,0 +1,79 @@ +package blackjack.controller + +import blackjack.model.Card +import blackjack.model.CardInfo +import blackjack.model.CardType +import blackjack.model.Participant +import blackjack.ui.InputView +import blackjack.ui.ResultView + +class BlackJackGame( + val participants: List, +) { + + private val cardsPool = mutableSetOf() + + init { + makeCardsPool() + allocateDefaultCards() + } + + fun allocateCards() { + participants.forEach { participant -> + while (participant.isPossibleToTakeMoreCard()) { + if (InputView.askCardPicking(participant.name)) { + allocateOneCard(participant) + ResultView.showStatusOfParticipant(participant) + } else { + ResultView.showStatusOfParticipant(participant) + break + } + } + } + } + + fun isPossibleToAllocation() = cardsPool.isNotEmpty() + + private fun makeCardsPool() { + CardType.values().forEach { type -> + CardInfo.values().forEach { cardInfo -> + cardsPool.add(Card(type, cardInfo)) + } + } + } + + private fun allocateDefaultCards() { + participants.forEach { + it.cards.addAll(pickRandomCards(DEFAULT_CARD_COUNTS)) + } + } + + fun allocateOneCard(participant: Participant) { + participant.cards.addAll(pickRandomCards(count = 1)) + } + + private fun pickRandomCards(count: Int): List { + val pickedCards = cardsPool.shuffled().take(count) + pickedCards.forEach { pickedCard -> + cardsPool.remove(pickedCard) + } + return pickedCards + } + + companion object { + const val DEFAULT_CARD_COUNTS = 2 + const val BEST_SCORE = 21 + } +} + +fun main() { + val participants = InputView.registerParticipants() + + val blackJackGame = BlackJackGame(participants) + + ResultView.showInitialStatusOfParticipants(participants) + + blackJackGame.allocateCards() + + ResultView.showGameResult(blackJackGame.participants) +} diff --git a/src/main/kotlin/blackjack/model/Card.kt b/src/main/kotlin/blackjack/model/Card.kt new file mode 100644 index 0000000000..634fd21c2a --- /dev/null +++ b/src/main/kotlin/blackjack/model/Card.kt @@ -0,0 +1,6 @@ +package blackjack.model + +data class Card( + val type: CardType, + val info: CardInfo, +) diff --git a/src/main/kotlin/blackjack/model/CardInfo.kt b/src/main/kotlin/blackjack/model/CardInfo.kt new file mode 100644 index 0000000000..1f39d32cbc --- /dev/null +++ b/src/main/kotlin/blackjack/model/CardInfo.kt @@ -0,0 +1,21 @@ +package blackjack.model + +enum class CardInfo( + val displayName: String, + val value1: Int, + val value2: Int = value1, +) { + Ace("A", 1, 11), + One("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), + King("K", 10), + Queen("Q", 10), + Jack("J", 10), +} diff --git a/src/main/kotlin/blackjack/model/CardType.kt b/src/main/kotlin/blackjack/model/CardType.kt new file mode 100644 index 0000000000..464c40e7fd --- /dev/null +++ b/src/main/kotlin/blackjack/model/CardType.kt @@ -0,0 +1,11 @@ +package blackjack.model + +enum class CardType( + val displayName: String, +) { + Diamond("다이아몬드"), + Spade("스페이드"), + Heart("하트"), + Clover("클로버"), + ; +} diff --git a/src/main/kotlin/blackjack/model/Participant.kt b/src/main/kotlin/blackjack/model/Participant.kt new file mode 100644 index 0000000000..2331599acf --- /dev/null +++ b/src/main/kotlin/blackjack/model/Participant.kt @@ -0,0 +1,35 @@ +package blackjack.model + +import blackjack.controller.BlackJackGame.Companion.BEST_SCORE +import kotlin.math.max +import kotlin.math.min + +data class Participant( + val name: String, + val cards: MutableList = mutableListOf(), +) { + fun isPossibleToTakeMoreCard(): Boolean { + return checkCurrentScore() < BEST_SCORE + } + + private fun checkCurrentScore(): Int { + return cards.sumOf { it.info.value1 } + } + + fun takeBestScore(): Int { + val scoreWithoutAce = cards.filter { it.info != CardInfo.Ace }.sumOf { it.info.value1 } + val countOfAce = cards.count { it.info == CardInfo.Ace } + var bestScore = scoreWithoutAce + countOfAce * CardInfo.Ace.value1 + (0 until countOfAce).forEach { countOfScoreOneAce -> + val scoreOfAces = + countOfScoreOneAce * CardInfo.Ace.value1 + (countOfAce - countOfScoreOneAce) * CardInfo.Ace.value2 + val totalScore = scoreWithoutAce + scoreOfAces + bestScore = if (totalScore <= BEST_SCORE) { + max(bestScore, totalScore) + } else { + min(bestScore, totalScore) + } + } + return bestScore + } +} diff --git a/src/main/kotlin/blackjack/ui/InputView.kt b/src/main/kotlin/blackjack/ui/InputView.kt new file mode 100644 index 0000000000..60cda5b501 --- /dev/null +++ b/src/main/kotlin/blackjack/ui/InputView.kt @@ -0,0 +1,28 @@ +package blackjack.ui + +import blackjack.model.Participant + +object InputView { + + fun registerParticipants(): List { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + return readlnOrNull() + ?.split(',') + ?.map { name -> + Participant(name) + } + ?.toList() + ?: throw IllegalArgumentException("참여자의 이름은 null을 허용하지 않습니다.") + } + + fun askCardPicking(name: String): Boolean { + println("${name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") + return readlnOrNull()?.let { + when (it) { + "y" -> true + "n" -> false + else -> throw IllegalArgumentException("카드 받기 여부는 y 또는 n만 입력 가능합니다.") + } + } ?: throw IllegalArgumentException("카드 받기 여부는 null을 허용하지 않습니다.") + } +} diff --git a/src/main/kotlin/blackjack/ui/ResultView.kt b/src/main/kotlin/blackjack/ui/ResultView.kt new file mode 100644 index 0000000000..8a00b66911 --- /dev/null +++ b/src/main/kotlin/blackjack/ui/ResultView.kt @@ -0,0 +1,42 @@ +package blackjack.ui + +import blackjack.controller.BlackJackGame +import blackjack.model.Participant + +object ResultView { + + fun showInitialStatusOfParticipants(participants: List) { + participants.forEach { participant -> + val separator = if (participant == participants.first()) "" else ", " + print("$separator${participant.name}") + } + println("에게 ${BlackJackGame.DEFAULT_CARD_COUNTS}장의 카드를 나눠주었습니다.") + + participants.forEach { participant -> + print("${participant.name}카드 : ") + showCards(participant) + println() + } + } + + fun showStatusOfParticipant(participant: Participant, useNewLine: Boolean = true) { + print("${participant.name}카드 : ") + showCards(participant) + if (useNewLine) println() + } + + private fun showCards(participant: Participant) { + participant.cards.forEach { + val postfix = if (it == participant.cards.last()) "" else ", " + print("${it.info.displayName}${it.type.displayName}$postfix") + } + } + + fun showGameResult(participants: List) { + println() + participants.forEach { participant -> + showStatusOfParticipant(participant, useNewLine = false) + println(" - 결과 : ${participant.takeBestScore()}") + } + } +} diff --git a/src/test/kotlin/DslTest.kt b/src/test/kotlin/DslTest.kt index 97c6924a63..265160eb1f 100644 --- a/src/test/kotlin/DslTest.kt +++ b/src/test/kotlin/DslTest.kt @@ -24,17 +24,16 @@ class DslTest { name shouldBe "greentea.latte" company shouldBe "kakao" softSkills shouldContainInOrder listOf( - "A passion for problem solving", - "Good communication skills" + Skill.SoftSkill("A passion for problem solving"), + Skill.SoftSkill("Good communication skills"), ) - hardSkills shouldContain "Kotlin" + hardSkills shouldContain Skill.HardSkill("Kotlin") languageLevels shouldContainInOrder listOf( "Korean" to 5, "English" to 3, ) } } - } fun introduce(block: PersonBuilder.() -> Unit): Person { @@ -45,8 +44,8 @@ class PersonBuilder { private lateinit var name: String private lateinit var company: String - private val softSkills = mutableListOf() - private val hardSkills = mutableListOf() + private val softSkills = mutableListOf() + private val hardSkills = mutableListOf() private val languageLevels = mutableListOf>() @@ -63,11 +62,11 @@ class PersonBuilder { } fun soft(value: String) { - softSkills.add(value) + softSkills.add(Skill.SoftSkill(value)) } fun hard(value: String) { - hardSkills.add(value) + hardSkills.add(Skill.HardSkill(value)) } fun languages(block: PersonBuilder.() -> Unit): PersonBuilder { @@ -92,7 +91,12 @@ class PersonBuilder { data class Person( val name: String, val company: String?, - val softSkills: List, - val hardSkills: List, + val softSkills: List, + val hardSkills: List, val languageLevels: List>, -) \ No newline at end of file +) + +sealed interface Skill { + data class SoftSkill(val name: String) : Skill + data class HardSkill(val name: String) : Skill +} diff --git a/src/test/kotlin/blackjack/controller/BlackJackGameTest.kt b/src/test/kotlin/blackjack/controller/BlackJackGameTest.kt new file mode 100644 index 0000000000..ba9a668ab4 --- /dev/null +++ b/src/test/kotlin/blackjack/controller/BlackJackGameTest.kt @@ -0,0 +1,108 @@ +package blackjack.controller + +import blackjack.controller.BlackJackGame.Companion.DEFAULT_CARD_COUNTS +import blackjack.model.Card +import blackjack.model.CardInfo +import blackjack.model.CardType +import blackjack.model.Participant +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +class BlackJackGameTest { + + @ParameterizedTest + @MethodSource("makeParticipants") + fun checkDefaultAllocatedCards(participants: List) { + val blackJackGame = BlackJackGame(participants) + + blackJackGame.participants.forEach { + it.cards.size shouldBe DEFAULT_CARD_COUNTS + } + } + + @ParameterizedTest + @MethodSource("makeParticipants") + fun checkAfterOneCardAllocation(participants: List) { + val blackJackGame = BlackJackGame(participants) + blackJackGame.allocateOneCard(participants[0]) + + participants[0].cards.size shouldBe (DEFAULT_CARD_COUNTS + 1) + } + + @Test + fun `카드를 더 뽑을 수 있는지 참여자인지 확인하는 용도로 현재 가지고 있는 카드의 합을 계산`() { + val participantScore20 = Participant( + "Paul", + mutableListOf( + Card(CardType.Clover, CardInfo.Ace), + Card(CardType.Clover, CardInfo.Nine), + Card(CardType.Clover, CardInfo.King), + ) + ) + + val participantScore21 = Participant( + "Ringo", + mutableListOf( + Card(CardType.Clover, CardInfo.Six), + Card(CardType.Clover, CardInfo.Seven), + Card(CardType.Clover, CardInfo.Eight), + ) + ) + + participantScore20.isPossibleToTakeMoreCard() shouldBe true + participantScore21.isPossibleToTakeMoreCard() shouldBe false + } + + @ParameterizedTest + @MethodSource("makeParticipants") + fun `더 나눠줄 카드가 있는지 확인`(participants: List) { + val blackJackGame = BlackJackGame(participants) + blackJackGame.isPossibleToAllocation() shouldBe true + + repeat(52) { + blackJackGame.allocateOneCard(participants[0]) + } + blackJackGame.isPossibleToAllocation() shouldBe false + } + + @Test + fun `최적의 점수`() { + val participant = Participant( + "John", + mutableListOf( + Card(CardType.Clover, CardInfo.Ace), + Card(CardType.Clover, CardInfo.Jack), + Card(CardType.Clover, CardInfo.King), + ) + ) + + val participant2 = Participant( + "George", + mutableListOf( + Card(CardType.Clover, CardInfo.One), + Card(CardType.Clover, CardInfo.Two), + Card(CardType.Clover, CardInfo.Three), + ) + ) + + participant.takeBestScore() shouldBe 21 + participant2.takeBestScore() shouldBe 6 + } + + companion object { + @JvmStatic + fun makeParticipants() = listOf( + Arguments.of( + listOf( + Participant("Liam"), + Participant("Noel"), + Participant("Gem"), + Participant("Andy"), + ) + ) + ) + } +}