-
Notifications
You must be signed in to change notification settings - Fork 313
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
feat(step2): 블랙잭 #692
base: david-sang
Are you sure you want to change the base?
feat(step2): 블랙잭 #692
Changes from all commits
3f269cc
ec50971
3574bae
3fe467d
b558652
6cdd3de
acda12d
d2a058e
d66fe54
356b1ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,37 @@ | ||
# kotlin-blackjack | ||
# kotlin-blackjack | ||
|
||
## 기능 요구사항 | ||
블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. | ||
|
||
- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. | ||
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. | ||
|
||
## 프로그래밍 요구 사항 | ||
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외 | ||
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. | ||
- 모든 엔티티를 작게 유지한다. | ||
- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. | ||
- 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다. | ||
- git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다. | ||
|
||
## 구현할 기능 목록 | ||
### 점수 | ||
- [x] 숫자 카드는 카드의 숫자를 점수로 계산한다 | ||
- [x] Ace 카드는 1 또는 11로 점수 계산 가능하다 | ||
- [x] King, Queen, Jack은 10으로 계산한다 | ||
|
||
### 시작 | ||
- [x] 입력한 이름을 기준으로 게임에 플레이어가 정해진다 (플레이어는 유일한 이름을 가진다) | ||
|
||
### 카드 분배 | ||
- [x] 플레이어는 최초에 카드 2장을 분배 받는다 | ||
- [x] 카드 분배는 최초 한 번 이루어진다 | ||
|
||
### 카드 추가 | ||
- [x] 보유한 카드의 점수 합이 블랙잭 기준치(21) 미만이면 추가로 카드를 받을 수 있는 상태이다 | ||
|
||
### 결과 | ||
- [x] 만들 수 있는 최종 점수가 모두 블랙잭 기준치(21)를 넘는다면 버스트이다과 | ||
- [x] 버스트인 경우, 최종 점수는 모든 카드를 더해서 만들 수 있는 가장 작은 수이다 | ||
- [x] 버스트가 아닌 경우, 최종 점수는 모든 카드를 더해서 블랙잭 기준치(21) 이하에서 만들 수 있는 가장 큰 수이다 | ||
- [x] 최종 점수가 블랙잭 기준치(21)이고 가진 카드가 2장이면 블랙잭이다 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package blackjack | ||
|
||
import blackjack.controller.BlackjackController | ||
|
||
fun main() { | ||
val controller = BlackjackController() | ||
val game = controller.startGame() | ||
repeat(game.players.values.size) { controller.progressPlayerPhase(game) } | ||
controller.progressEndPhase(game) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package blackjack.controller | ||
|
||
import blackjack.domain.Game | ||
import blackjack.domain.Phase | ||
import blackjack.view.InputView | ||
import blackjack.view.OutputView | ||
|
||
class BlackjackController { | ||
fun startGame(): Game { | ||
val names = InputView.getNames() | ||
val game = Game(names) | ||
game.players.values.forEach { | ||
OutputView.printGameInitialization(game) | ||
OutputView.printPlayerAndCard(it) | ||
Comment on lines
+13
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OutputView의 함수를 하나하나 실행하기보다, OutputView가 텍스트가 아닌, UI로 변경된다면 Controller의 로직 또한 변경되야할까요? |
||
} | ||
return game | ||
} | ||
|
||
fun progressPlayerPhase(game: Game) { | ||
val phase = game.checkAndGetPhase(Phase.PlayerPhase::class) | ||
while (!phase.isFinish()) { | ||
when (InputView.getHitOrStay(phase.player.name)) { | ||
"y" -> phase.player.hit(game.deck) | ||
"n" -> phase.player.stay() | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. y,n 문자열또한 View의 책임 아닐까요? |
||
else -> throw IllegalArgumentException("only 'y' or 'n' can be entered") | ||
} | ||
OutputView.printPlayerAndCard(phase.player) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. 프로그램 요구사항을 지켜보면 어떨까요? |
||
game.moveToNextPhase() | ||
} | ||
|
||
fun progressEndPhase(game: Game) { | ||
val phase = game.checkAndGetPhase(Phase.EndPhase::class) | ||
phase.players.values.forEach { | ||
OutputView.printPlayerAndCardAndScore(it) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package blackjack.domain | ||
|
||
data class Card(val suit: Suit, val rank: Rank) { | ||
val possibleScore: PossibleScore = PossibleScore.getPossibleScore(rank) | ||
|
||
fun getPossibleScoreSums(score: Score): List<Score> = possibleScore.getPossibleScoreSums(score) | ||
} | ||
|
||
enum class Suit { | ||
HEART, | ||
DIAMOND, | ||
SPADE, | ||
CLUB; | ||
} | ||
|
||
enum class Rank(val number: Int) { | ||
ACE(1), | ||
TWO(2), | ||
THREE(3), | ||
FOUR(4), | ||
FIVE(5), | ||
SIX(6), | ||
SEVEN(7), | ||
EIGHT(8), | ||
NINE(9), | ||
TEN(10), | ||
JACK(11), | ||
QUEEN(12), | ||
KING(13); | ||
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. number는 무엇을 의미하나요? |
||
} | ||
|
||
@JvmInline | ||
value class PossibleScore(private val values: Set<Score>) { | ||
|
||
fun getPossibleScoreSums(score: Score): List<Score> = values.map { score + it } | ||
|
||
companion object { | ||
fun getPossibleScore(rank: Rank) = when (rank) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코틀린에서 리턴타입을 생략할수 있지만, |
||
Rank.ACE -> listOf(1, 11) | ||
Rank.TWO -> listOf(2) | ||
Rank.THREE -> listOf(3) | ||
Rank.FOUR -> listOf(4) | ||
Rank.FIVE -> listOf(5) | ||
Rank.SIX -> listOf(6) | ||
Rank.SEVEN -> listOf(7) | ||
Rank.EIGHT -> listOf(8) | ||
Rank.NINE -> listOf(9) | ||
Rank.TEN -> listOf(10) | ||
Rank.JACK -> listOf(10) | ||
Rank.QUEEN -> listOf(10) | ||
Rank.KING -> listOf(10) | ||
} | ||
Comment on lines
+38
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
.map(Score::from) | ||
.toSet() | ||
.let(::PossibleScore) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package blackjack.domain | ||
|
||
@JvmInline | ||
value class Deck(private val cards: MutableSet<Card>) { | ||
fun draw(): Card { | ||
check(cards.isNotEmpty()) { "no cards left" } | ||
val card = cards.random() | ||
cards.remove(card) | ||
return card | ||
} | ||
|
||
companion object { | ||
private val preparedDeck: Set<Card> = | ||
Suit.values().flatMap { suit -> | ||
Rank.values().map { rank -> | ||
Card(suit = suit, rank = rank) | ||
} | ||
}.toSet() | ||
|
||
fun getDeck(): Deck = preparedDeck.toMutableSet().let(::Deck) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package blackjack.domain | ||
|
||
import kotlin.reflect.KClass | ||
|
||
class Game(val players: Players, val deck: Deck = Deck.getDeck()) { | ||
|
||
private val phases: List<Phase> | ||
private var phaseIndex: Int = 0 | ||
private val phase | ||
get() = phases[phaseIndex] | ||
Comment on lines
+7
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. phase를 구분하신 이유가 있을까요? |
||
|
||
constructor(names: List<String>) : this(names.map(::Player).let(::Players)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생성자보단, 팩토리함수를 활용해보면 어떨까요>? 참고해보아요! |
||
|
||
init { | ||
players.values.forEach { | ||
it.initialize(deck) | ||
} | ||
phases = players.values.map(Phase::PlayerPhase) + players.let(Phase::EndPhase) | ||
} | ||
|
||
fun moveToNextPhase() { | ||
check(phase.isFinish()) { "current phase is not over" } | ||
check(phaseIndex < phases.size) { "there is no more next phase" } | ||
phaseIndex++ | ||
} | ||
|
||
fun <T : Phase> checkAndGetPhase(phaseType: KClass<T>): T { | ||
check(phaseType.isInstance(phase)) { "not ${phaseType::class.java.name}" } | ||
return phase as T | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package blackjack.domain | ||
|
||
@JvmInline | ||
value class Hand(val cards: MutableList<Card> = mutableListOf()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cards는 외부에서 접근가능하여 변경 가능한 구조네요! |
||
|
||
fun add(card: Card) = cards.add(card) | ||
|
||
fun getCardCount() = cards.size | ||
|
||
fun getBestScore(): Score { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 블랙잭의 룰에서는 Ace는 21을 초과하지 않을때는 11, 21이 초과될떄는 1로 계산되고 있어요, 자유롭게 참고해보셔도 좋을거같아요
간단하게 기본적으로 Ace를 1로 계산하고, |
||
val scoreSums = mutableSetOf<Score>() | ||
calculateScoreSums(cards.toList(), 0, Score.from(0), scoreSums) | ||
return when (scoreSums.minBy { it.value }.value <= Rule.BLACKJACK_SCORE) { | ||
true -> { | ||
scoreSums | ||
.filter { it.value <= Rule.BLACKJACK_SCORE } | ||
.maxBy { it.value } | ||
} | ||
false -> { | ||
scoreSums.minBy { it.value } | ||
} | ||
} | ||
} | ||
|
||
private fun calculateScoreSums(cards: List<Card>, index: Int, sum: Score, sums: MutableSet<Score>) { | ||
if (index == cards.size) { | ||
sums.add(sum) | ||
return | ||
} | ||
cards[index].getPossibleScoreSums(sum).forEach { | ||
calculateScoreSums(cards, index + 1, it, sums) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package blackjack.domain | ||
|
||
sealed interface Phase { | ||
fun isFinish(): Boolean | ||
|
||
class PlayerPhase(val player: Player) : Phase { | ||
override fun isFinish(): Boolean = player.resolved() | ||
} | ||
|
||
class EndPhase(val players: Players) : Phase { | ||
override fun isFinish(): Boolean = true | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package blackjack.domain | ||
|
||
data class Player(val name: String, val hand: Hand = Hand()) { | ||
var state: PlayerState = PlayerState.READY | ||
private set | ||
|
||
val resultScore: Score by lazy { decideScore() } | ||
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. resultScore 는 hand에 따라 상태가 관리되는 함수가 아닌가요?
state 또한 stay를 제외하고는 hand 상태에 따라 상태가 변경되는 구조네요, |
||
|
||
fun initialize(deck: Deck) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. deck을 파라미터로 받기보다, draw된 cards를 넘겨받는 구조는 어떨까요? |
||
check(state == PlayerState.READY) { "can only 'init' if the 'PlayerState' is 'READY'" } | ||
repeat(Rule.INIT_CARD_COUNT) { hand.add(deck.draw()) } | ||
state = PlayerState.UNDER | ||
} | ||
|
||
fun hit(deck: Deck) { | ||
check(active()) { "can only 'hit' if the 'PlayerState' is 'UNDER'" } | ||
hand.add(deck.draw()) | ||
updateState() | ||
} | ||
|
||
fun stay() { | ||
check(active()) { "can only 'stay' if the 'PlayerState' is 'UNDER'" } | ||
state = PlayerState.STAND | ||
} | ||
|
||
fun updateState() { | ||
val score = hand.getBestScore() | ||
val count = hand.getCardCount() | ||
state = when { | ||
score.value < Rule.BLACKJACK_SCORE -> PlayerState.UNDER | ||
score.value > Rule.BLACKJACK_SCORE -> PlayerState.BUST | ||
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. score의 Value에 접근하기보단, https://dkswnkk.tistory.com/687 참고해보아요! |
||
else -> when (count == Rule.BLACKJACK_CARD_COUNT) { | ||
true -> PlayerState.BLACKJACK | ||
false -> PlayerState.STAND | ||
} | ||
} | ||
} | ||
|
||
fun ready() = state == PlayerState.READY | ||
|
||
fun active() = state == PlayerState.UNDER | ||
|
||
fun resolved() = state == PlayerState.STAND || state == PlayerState.BLACKJACK || state == PlayerState.BUST | ||
|
||
private fun decideScore(): Score { | ||
check(resolved()) { "can't decide score until the action is over" } | ||
return hand.getBestScore() | ||
} | ||
} | ||
|
||
@JvmInline | ||
value class Players(val values: List<Player>) { | ||
|
||
constructor(vararg names: String) : this(names.map { Player(it) }) | ||
|
||
init { | ||
require(values.size == values.map { it.name }.toSet().size) { "duplicate name has been used" } | ||
} | ||
} | ||
|
||
enum class PlayerState { | ||
READY, | ||
UNDER, | ||
STAND, | ||
BLACKJACK, | ||
BUST; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package blackjack.domain | ||
|
||
object Rule { | ||
const val BLACKJACK_SCORE = 21 | ||
const val BLACKJACK_CARD_COUNT = 2 | ||
const val INIT_CARD_COUNT = 2 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package blackjack.domain | ||
|
||
|
||
@JvmInline | ||
value class Score private constructor(val value: Int) { | ||
|
||
operator fun plus(score: Score) = from(value + score.value) | ||
|
||
companion object { | ||
private const val MIN_SCORE = 0 | ||
private const val MAX_SCORE = 100 | ||
private val preparedScores = (MIN_SCORE..MAX_SCORE).associateWith(::Score) | ||
|
||
fun from(value: Int): Score = preparedScores[value] ?: Score(value) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package blackjack.view | ||
|
||
object InputView { | ||
fun getNames(): List<String> { | ||
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") | ||
return readln().split(",") | ||
} | ||
|
||
fun getHitOrStay(name: String): String { | ||
println("${name}은(는) 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") | ||
return readln() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
main함수의 코드를 최소화 해보면 어떨까요?
controller의 함수를 통해 리턴된 game을 다시 controller의 함수의 파라미터로 사용하는 등의 동작은
불필요한 책임 아닐까요? 모두 BlackjackController에게 위임해보면 어떨까요?
또한, 게임이 시작 된 이후, 플레이들이 카드를 더받고 등의 로직은 비즈니스로직이 아닐까요?