Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

블랙잭 2단계 구현 #615

Open
wants to merge 15 commits into
base: bsgreentea
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# kotlin-blackjack
# kotlin-blackjack

게임 관련 기능

- [x] 카드 정보 클래스 구성
- [x] 참여자 클래스 구성
- [x] 기본 카드 나눠주기(게임 시작과 함께 2장씩 나눠준다.)
- [x] 카드를 나눠줄 수 있는 상태 확인(나눠줄 카드가 남아있는지, 현재 점수가 21점보다 작은지 판단)
- [x] y/n 응답 별 카드 배분
- [x] 최적의 점수 구하기

입출력 기능

- [x] 참여자 입력 구현
- [x] 현재 카드 상태 출력
- [x] y/n 응답 묻기
- [x] 결과 출력
79 changes: 79 additions & 0 deletions src/main/kotlin/blackjack/controller/BlackJackGame.kt
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller 역할을 하고 있는 BlackJackGame 내에서 분리해볼 수 있는 역할/책임이 있을까요?
다시 말해서, Controller를 만약 테스트하지 않는다고 하면(관련 코멘트는 따로 드릴게요) 테스트 범위에서 벗어나는 게임 요구사항이 있을까요?

val participants: List<Participant>,
) {

private val cardsPool = mutableSetOf<Card>()

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트에서만 활용되는 함수네요.

테스트에서만 사용되는 로직을 위해 비즈니스 로직을 public으로 변경하는것은 좋지 않습니다. 가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여길 수 있으나 가독성 이상의 역할을 하는 경우 testable하게 구현하기 위해서는 클래스 분리를 할 시점은 아닐지 고민해볼 수 있습니다. 🙂


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<Card> {
val pickedCards = cardsPool.shuffled().take(count)
pickedCards.forEach { pickedCard ->
cardsPool.remove(pickedCard)
}
return pickedCards
}
Comment on lines +55 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트하기 어려운 로직을 어떻게 테스트할 수 있을까요?
혹시 자동차 경주/로또 미션에서는 이런 로직을 어떻게 테스트하셨나요?


companion object {
const val DEFAULT_CARD_COUNTS = 2
const val BEST_SCORE = 21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 상수값을 들고 있는 책임이 Controller인게 적절할까요?

}
}

fun main() {
val participants = InputView.registerParticipants()

val blackJackGame = BlackJackGame(participants)

ResultView.showInitialStatusOfParticipants(participants)

blackJackGame.allocateCards()

ResultView.showGameResult(blackJackGame.participants)
}
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/model/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.model

data class Card(
val type: CardType,
val info: CardInfo,
)
21 changes: 21 additions & 0 deletions src/main/kotlin/blackjack/model/CardInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackjack.model

enum class CardInfo(
val displayName: String,
val value1: Int,
val value2: Int = value1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A를 1로 처리할지, 11로 처리할지 구분하는 로직 작성이 이번 미션의 핵심 중 하나입니다!
다른 개발자들도 CardInfo의 value1, value2를 보고 각각 어떤 값인지 이해할 수 있을까요?

) {
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),
}
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/model/CardType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack.model

enum class CardType(
val displayName: String,
) {
Diamond("다이아몬드"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 뷰 요구사항이 수정되어 "Diamond"로 출력해야 한다고 가정해봅시다!
뷰 요구사항이 수정되었는데 Model이 변경되는게 어색하지 않으신가요?
자동차 경주 미션 5단계의 MVC 패턴을 참고하여 domain 로직과 view 로직의 의존 방향을 구분해보면 어떨까요?

Spade("스페이드"),
Heart("하트"),
Clover("클로버"),
;
}
35 changes: 35 additions & 0 deletions src/main/kotlin/blackjack/model/Participant.kt
Original file line number Diff line number Diff line change
@@ -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<Card> = 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 ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repeat을 사용해보면 어떨까요?

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
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/blackjack/ui/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackjack.ui

import blackjack.model.Participant

object InputView {

fun registerParticipants(): List<Participant> {
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을 허용하지 않습니다.")
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/blackjack/ui/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package blackjack.ui

import blackjack.controller.BlackJackGame
import blackjack.model.Participant

object ResultView {

fun showInitialStatusOfParticipants(participants: List<Participant>) {
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<Participant>) {
println()
participants.forEach { participant ->
showStatusOfParticipant(participant, useNewLine = false)
println(" - 결과 : ${participant.takeBestScore()}")
}
}
}
26 changes: 15 additions & 11 deletions src/test/kotlin/DslTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -45,8 +44,8 @@ class PersonBuilder {
private lateinit var name: String
private lateinit var company: String

private val softSkills = mutableListOf<String>()
private val hardSkills = mutableListOf<String>()
private val softSkills = mutableListOf<Skill.SoftSkill>()
private val hardSkills = mutableListOf<Skill.HardSkill>()

private val languageLevels = mutableListOf<Pair<String, Int>>()

Expand All @@ -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 {
Expand All @@ -92,7 +91,12 @@ class PersonBuilder {
data class Person(
val name: String,
val company: String?,
val softSkills: List<String>,
val hardSkills: List<String>,
val softSkills: List<Skill.SoftSkill>,
val hardSkills: List<Skill.HardSkill>,
val languageLevels: List<Pair<String, Int>>,
)
)

sealed interface Skill {
data class SoftSkill(val name: String) : Skill
data class HardSkill(val name: String) : Skill
}
Comment on lines +99 to +102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sealed interface를 잘 사용해주셨습니다! enum과 어떤 점이 다른지 눈치채셨나요?
참고: https://blog.kotlin-academy.com/enum-vs-sealed-class-which-one-to-choose-dc92ce7a4df5

앞으로 블랙잭 미션을 진행하면서 활용해볼 곳이 있는지 고민해보시면 좋을 것 같아요!