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

[Step3] 블랙잭(딜러) #675

Open
wants to merge 8 commits into
base: yibeomseok
Choose a base branch
from
44 changes: 41 additions & 3 deletions docs/BLACKJACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,44 @@ jason카드: 7클로버, K스페이드 - 결과: 17

### 3. 게임 규칙 및 결과 판정

- [ ] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다.
- [ ] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다.
- [ ] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다.
- [x] 21점 초과 판정 로직: 플레이어 또는 딜러의 손패 합계가 21을 초과하면 게임에서 패배한다.
- [x] 승리 조건 계산: 플레이어와 딜러 중 21에 가장 근접한 쪽이 승리한다.
- [x] 게임 결과 출력: 최종 승자와 각 플레이어의 최종 손패 및 점수를 출력한다.

## 딜러가 추가된 사전 구현 계획

- [x] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
- [x] 만일 딜러가 <Ace, 6>을 갖고 있다면?
- [x] 딜러는 다양한 전략을 사용할 수 있도록 한다.

### 도메인 모델들이 결정되었다면 블랙잭 게임 구현 계획
도메인 모듈이 뷰에게 의존하지 않도록 구현할 것이다.

- [x] **게임 초기화 상태 확인**
- [x] blackjackGame이 게임을 초기화해야 하는 상태인지 확인
- [x] 필요한 경우, blackjackGame에 필요한 정보를 사용자로부터 받아 전달

- [x] **플레이어의 턴 상태 확인 (첫 번째)**
- [x] blackjackGame이 플레이어들의 턴인 상태인지 확인
- [x] 현재 blackjackGame의 플레이어 중 누구의 턴인지 확인
- [x] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달

- [x] **플레이어의 턴 상태 재확인 (두 번째)**
- [x] 다시 blackjackGame이 플레이어들의 턴인 상태인지 확인
- [x] 다시 현재 플레이어 중 누구의 턴인지 확인
- [x] 해당 플레이어가 카드를 뽑을 것인지 말 것인지를 사용자로부터 받아 전달

- [x] **마지막 플레이어의 턴과 딜러의 턴 상태 전환**
- [x] 마지막 플레이어의 턴에서 더 이상 카드를 뽑을 수 없거나, 뽑지 않겠다는 결정이 내려진 경우 확인
- [x] blackjackGame의 상태를 딜러의 턴 상태로 전환

- [x] **딜러의 턴 상태 확인**
- [x] blackjackGame이 딜러의 턴인 상태인지 확인
- [x] 딜러가 카드를 뽑을지 안 뽑을지 결정하고 사용자에게 알림
- [x] blackjackGame을 `End` 상태로 전환

- [x] **End 상태 확인**
- [x] blackjackGame이 End 상태인지 확인
- [x] blackjackGame에게 BlackjackResult를 요청하고 사용자에게 보여줌

- [x] **게임 종료**
6 changes: 6 additions & 0 deletions domain/src/main/kotlin/action/BlackJackAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package action

enum class BlackJackAction {
HIT,
STAND,
}
9 changes: 9 additions & 0 deletions domain/src/main/kotlin/blackjack/BlackjackParticipant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackjack

import blackjack.card.Card

interface BlackjackParticipant {
fun receiveCard(card: Card): BlackjackParticipant
fun receiveCard(cards: List<Card>): BlackjackParticipant
fun calculateBestValue(): Int
}
26 changes: 26 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package blackjack.dealer

import action.BlackJackAction
import blackjack.BlackjackParticipant
import blackjack.card.Card
import blackjack.deck.Deck
import blackjack.hand.Hand
import blackjack.hand.StandardHand

data class Dealer(
val dealerStrategy: DealerStrategy = DefaultDealerStrategy(),
private val hand: Hand = StandardHand(),
) : BlackjackParticipant {

val cards: List<Card> get() = hand.cards()

override fun receiveCard(card: Card): Dealer = copy(hand = hand.addCard(card))

override fun receiveCard(cards: List<Card>): Dealer = copy(hand = hand.addCard(cards))

override fun calculateBestValue(): Int = hand.calculateBestValue()

fun decideAction(deck: Deck): BlackJackAction {
return dealerStrategy.decideAction(hand, deck)
}
}
9 changes: 9 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/DealerStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackjack.dealer

import action.BlackJackAction
import blackjack.deck.Deck
import blackjack.hand.Hand

interface DealerStrategy {
fun decideAction(hand: Hand, deck: Deck): BlackJackAction
}
Comment on lines +7 to +9
Copy link
Member

Choose a reason for hiding this comment

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

현재 DefaultDealerStrategy 만 사용되는데, 굳이 인터페이스로 분리가 필요했을까요? 🤔

37 changes: 37 additions & 0 deletions domain/src/main/kotlin/blackjack/dealer/DefaultDealerStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package blackjack.dealer

import action.BlackJackAction
import blackjack.card.Card
import blackjack.card.CardRank
import blackjack.deck.Deck
import blackjack.hand.Hand

internal class DefaultDealerStrategy : DealerStrategy {
override fun decideAction(hand: Hand, deck: Deck): BlackJackAction {
val dealerScore = hand.calculateBestValue()
val dealerMinScore = hand.calculateMinValue()

val bustingProbability = maxOf(
calculateProbabilityOfBusting(dealerScore, deck),
calculateProbabilityOfBusting(dealerMinScore, deck)
)

return if (bustingProbability > 0.5) BlackJackAction.STAND else BlackJackAction.HIT
}

private fun calculateProbabilityOfBusting(currentScore: Int, deck: Deck): Double {
val remainedScore = 21 - currentScore
val safeCards = deck.remainingCards.count { isSafe(it, remainedScore) }

return 1.0 - safeCards.toDouble() / deck.remainingCards.size
}

private fun isSafe(card: Card, remainedScore: Int): Boolean {
val cardValue = when (card.rank) {
CardRank.KING, CardRank.QUEEN, CardRank.JACK -> 10
CardRank.ACE -> 11
else -> card.rank.ordinal + 1
}
return cardValue <= remainedScore
}
}
11 changes: 8 additions & 3 deletions domain/src/main/kotlin/blackjack/deck/Deck.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package blackjack.deck

import blackjack.card.Card
import java.util.Stack
import java.util.*

class Deck(
cardProvider: CardProvider = StandardCardProvider(),
Expand All @@ -11,11 +11,16 @@ class Deck(
addAll(cardShuffler.shuffle(cardProvider.provideCards()))
}

val size
get() = cards.size
val remainingCards: List<Card>
get() = cards.toList()

fun drawCard(): Card {
check(cards.isNotEmpty()) { "덱에 카드가 없으면 카드를 뽑을 수 없습니다." }
return cards.pop()
}

fun drawCard(count: Int): List<Card> {
check(cards.size >= count) { "덱에 $count 만큼 카드가 없습니다." }
return List(count) { cards.pop() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package blackjack.deck

import blackjack.card.Card

class RandomCardShuffler : CardShuffler {
internal class RandomCardShuffler : CardShuffler {
override fun shuffle(cards: List<Card>): List<Card> = cards.shuffled()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import blackjack.card.Card
import blackjack.card.CardRank
import blackjack.card.CardSuit

class StandardCardProvider : CardProvider {
internal class StandardCardProvider : CardProvider {
Copy link
Member

Choose a reason for hiding this comment

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

internal 키워드 활용!
internal class 와 class 의 차이는 무엇인가요?

override fun provideCards(): List<Card> =
CardSuit.values().flatMap { suit ->
CardRank.values().map { rank -> Card(suit, rank) }
Expand Down
147 changes: 147 additions & 0 deletions domain/src/main/kotlin/blackjack/game/BlackjackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package blackjack.game

import action.BlackJackAction
import blackjack.BlackjackParticipant
import blackjack.card.Card
import blackjack.dealer.Dealer
import blackjack.dealer.DealerStrategy
import blackjack.dealer.DefaultDealerStrategy
import blackjack.deck.Deck
import blackjack.player.Player

class BlackjackGame private constructor(
players: List<Player>,
dealer: Dealer = Dealer(),
private val deck: Deck = Deck(),
) {
init {
require(players.toSet().isNotEmpty()) { "플레이어가 최소 한 명은 존재해야 합니다." }
}

var state: GameState = GameState.InitialDeal(players, dealer)
private set

val players: List<Player> get() = state.players
val dealer: Dealer get() = state.dealer

fun dealInitialCards() {
check(state is GameState.InitialDeal) { "Initial Deal 상태가 아닙니다." }
val nPlayers = List(players.size) { players[it].receiveCard(deck.drawCard(2)) }
val nDealer = dealer.receiveCard(deck.drawCard(2))
state = GameState.PlayerTurn(nPlayers, nDealer, currentPlayerIndex = 0)
}

fun dealPlayerTurn(player: Player, isDeal: Boolean) {
val playerTurnState = state as? GameState.PlayerTurn ?: throw IllegalStateException("Player Turn이 아닙니다.")
require(players.contains(player)) { "${player.name}이라는 플레이어는 없습니다." }
require(player == playerTurnState.currentPlayer) { "현재 턴은 ${player.name}의 턴이 아닙니다." }

if (isDeal.not()) {
// 다음 플레이어로 넘어감
moveToNextPlayerOrDealerTurn(playerTurnState.currentPlayerIndex)
} else {
// 카드 받기
check(player.canHit() == BlackJackAction.HIT) { "해당 플레이어는 더 이상 카드를 받을 수 없습니다." }
val nPlayers = players.map { if (it == player) it.receiveCard(deck.drawCard()) else it }
Comment on lines +39 to +45
Copy link
Member

Choose a reason for hiding this comment

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

불필요한 주석은 제거해주세요!
만약 주석이 필요하다면 코드가 너무 복잡한게 아닌지 고민해봐야겠어요 🤔
복잡한 코드를 개선하기 위한 방법은 무엇이 있을까요?

state = GameState.PlayerTurn(nPlayers, dealer, playerTurnState.currentPlayerIndex)
}
}

fun dealDealerTurn(): BlackJackAction {
check(state is GameState.DealerTurn) { "Dealer Turn이 아닙니다." }
val dealerAction = dealer.decideAction(deck)
return if (dealerAction == BlackJackAction.HIT) {
val drawnCard = deck.drawCard()
state = GameState.End(players, dealer.receiveCard(drawnCard))
BlackJackAction.HIT
} else {
state = GameState.End(players, dealer)
BlackJackAction.STAND
}
}

fun calculateResult(): Map<BlackjackParticipant, BlackjackResult> {
val results = mutableMapOf<BlackjackParticipant, BlackjackResult>()
results[dealer] = calculateDealerResult()
players.forEach { results[it] = calculatePlayerResult(it) }
return results
}

fun showPlayerCards(playerName: String): List<Card> {
val player = state.players.find { it.name == playerName }
?: throw IllegalArgumentException("${playerName}이라는 플레이어는 없습니다.")
return player.cards
}

private fun calculateDealerResult(): BlackjackResult {
val dealerScore = dealer.calculateBestValue()
var win = 0
var loss = 0
players.forEach {
if (dealerScore > 21) loss++
else if (it.calculateBestValue() > 21) win++
else if (dealerScore > it.calculateBestValue()) win++
else if (dealerScore <= it.calculateBestValue()) loss++
}
return BlackjackResult(win, loss)
}
Comment on lines +63 to +87
Copy link
Member

Choose a reason for hiding this comment

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

딜러기준 승패를 계산하는 로직을 손보기 위해선 BlackjackGame 코드를 76번 라인까지 확인해야겠어요.
처음 코드를 확인하는 동료 개발자 입장에선 원하는 로직을 확인하기 어려울 것 같은데, 이를 개선할 수 있는 방법이 있을까요?


private fun calculatePlayerResult(player: Player): BlackjackResult {
val playerScore = player.calculateBestValue()
val dealerScore = dealer.calculateBestValue()
return if (dealerScore > 21) {
BlackjackResult(1, 0)
} else if (playerScore > 21) {
BlackjackResult(0, 1)
} else if (playerScore >= dealerScore) {
BlackjackResult(1, 0)
} else {
BlackjackResult(0, 1)
}
}
Comment on lines +89 to +101
Copy link
Member

Choose a reason for hiding this comment

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

else 예약어를 쓰지 않는다.

객체지향 생활체조 원칙에 따라 요 코드도 개선해보면 좋겠네요!
https://edu.nextstep.camp/s/lepg4Qrl/ls/QZaMqdGU


private fun moveToNextPlayerOrDealerTurn(currentPlayerIndex: Int) {
val nextPlayerIndex = (currentPlayerIndex + 1) % players.size
state = if (nextPlayerIndex == 0) {
GameState.DealerTurn(players, dealer)
} else {
GameState.PlayerTurn(players, dealer, nextPlayerIndex)
}
}

class BlackjackGameBuilder {
private val players: MutableList<Player> = mutableListOf()
private var dealerStrategy: DealerStrategy = DefaultDealerStrategy()

fun join(name: String) {
players.add(Player(name = name))
}

fun join(names: List<String>) {
names.forEach {
join(it)
}
}

fun dealerStrategy(strategy: DealerStrategyType) {
when (strategy) {
DealerStrategyType.DEFAULT_DEALER_STRATEGY -> dealerStrategy = DefaultDealerStrategy()
// 다른 전략 추가
}
}

fun build(): BlackjackGame {
return BlackjackGame(
players = players.toList(),
dealer = Dealer(dealerStrategy = dealerStrategy)
)
}
}
}

enum class DealerStrategyType {
DEFAULT_DEALER_STRATEGY
}

fun blackjackOpen(block: BlackjackGame.BlackjackGameBuilder.() -> Unit): BlackjackGame =
BlackjackGame.BlackjackGameBuilder().apply(block).build()
6 changes: 6 additions & 0 deletions domain/src/main/kotlin/blackjack/game/BlackjackResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.game

data class BlackjackResult(
val win: Int,
val lose: Int,
)
33 changes: 33 additions & 0 deletions domain/src/main/kotlin/blackjack/game/GameState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package blackjack.game

import blackjack.dealer.Dealer
import blackjack.player.Player

sealed class GameState(
val players: List<Player>,
val dealer: Dealer,
) {
class InitialDeal(
players: List<Player>,
dealer: Dealer,
) : GameState(players, dealer)

class PlayerTurn(
players: List<Player>,
dealer: Dealer,
val currentPlayerIndex: Int,
) : GameState(players, dealer) {
val currentPlayer: Player
get() = players[currentPlayerIndex]
}

class DealerTurn(
players: List<Player>,
dealer: Dealer,
) : GameState(players, dealer)

class End(
players: List<Player>,
dealer: Dealer,
) : GameState(players, dealer)
}
Loading