Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dc0a080
[코드리뷰][step3] 현재 카드의 합이 21이 넘지 않는 경우 y 를 선택했을 때 카드를 뽑도록 로직 수정
SaraHan774 Dec 16, 2024
0f27917
[코드리뷰][step3] 매직넘버 const 로 대체
SaraHan774 Dec 16, 2024
daa723b
[코드리뷰][step3] PlayerResult -> GameResult 로 변수명 변경, GameResult 에 대한 카운…
SaraHan774 Dec 16, 2024
4fd6961
[코드리뷰][step3] PlayerResultCalculator class -> object class 로 변경, Blac…
SaraHan774 Dec 16, 2024
126be39
[코드리뷰][step3] 프로그래밍 제약 조건을 만족하기 위해 while 문의 if 제거
SaraHan774 Dec 16, 2024
ce186a3
[코드리뷰][step3] 테스트 오류 수정, Player 클래스 버그 수정
SaraHan774 Dec 16, 2024
8b26a92
[코드리뷰][step3] 테스트 코드 수정: DisplayName 사용 해보기, Parameterized 테스트 적용 가능한…
SaraHan774 Dec 16, 2024
ec35146
[코드리뷰][step3] ktlintformat
SaraHan774 Dec 16, 2024
38c0b9d
[step4] 플레이어는 게임을 시작할 때 베팅 금액을 입력받는다
SaraHan774 Dec 16, 2024
995344b
[step4] 베팅 관련 규칙을 추가
SaraHan774 Dec 16, 2024
3e26696
[step4] Dealer 의 베팅 금액 설정 로직 수정
SaraHan774 Dec 16, 2024
9944f63
[step4] 로그 삭제
SaraHan774 Dec 16, 2024
17e1d6b
[step4] 계산로직 버그 수정
SaraHan774 Dec 16, 2024
bf2dcc2
[step4] 테스트 수정
SaraHan774 Dec 16, 2024
ae042c1
[step4] ktlintformat
SaraHan774 Dec 16, 2024
1ab98c9
[step4] 테스트 보강
SaraHan774 Dec 16, 2024
6861336
[step4] 결과 추출하는 부분 리팩토링
SaraHan774 Dec 16, 2024
d6ac32a
[step4] 주석 보강
SaraHan774 Dec 16, 2024
204492f
[step4] ktlintformat
SaraHan774 Dec 16, 2024
c2de14a
[step4] 딜러 수익 계산 로직 버그 수정
SaraHan774 Dec 16, 2024
7f3f1ce
[step4] 딜러 수익 계산 로직 버그 수정 - 불필요한 필드 제거
SaraHan774 Dec 16, 2024
4ab56d9
[step4] GameResult 파일 분리
SaraHan774 Dec 16, 2024
4d5b3ca
[step4] 확장함수 제거, 중복 제거
SaraHan774 Dec 16, 2024
8876e4b
[step4] Participant 책임 변경
SaraHan774 Dec 16, 2024
e77f4f5
[step4] 암시적으로 Dealer 의 수익을 세팅하는 부분을 명시적으로 변경
SaraHan774 Dec 16, 2024
28a20fa
[코드리뷰][step4] Deprecated 클래스 및 함수 제거
SaraHan774 Dec 16, 2024
334f5d8
[코드리뷰][step4] GameResult 로부터 베팅금액 반환하는 로직을 GameResult의 책임으로 이동시킴
SaraHan774 Dec 16, 2024
4a2a268
[코드리뷰][step4] ResultManager 생성자 주입에서 메서드주입으로 변경
SaraHan774 Dec 17, 2024
7c81404
[코드리뷰][step4] 일부 구조 변경
SaraHan774 Dec 19, 2024
3c5be6d
[코드리뷰][step4] profitMoney 는 Player 가 관리하도록 함
SaraHan774 Dec 19, 2024
223331d
[코드리뷰][step4] lint
SaraHan774 Dec 19, 2024
fde1866
[코드리뷰][step4] test 오류 수정
SaraHan774 Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,21 @@
- 기능 요구사항 바탕으로 테스트코드 작성
- 테스트코드 컴파일 되도록 코드 수정
- 게임의 주체를 나타내는 Player 객체 정의
- 블랙잭 카드를 표현할 수 있는 객체 정의
- 블랙잭 카드를 표현할 수 있는 객체 정의


- 3단계 목록이 날라갔네 ..

### 4단계 구현 기능 목록

- [x] 플레이어는 게임을 시작할 때 베팅 금액을 정해야 한다.
- 베팅 관련 규칙 추가
- [x] 카드를 추가로 뽑아 21을 초과할 경우 베팅 금액을 모두 잃게 된다.
- [x] 처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다.
- [x] 딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다.
- [x] 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다.

### 4단계 프로그래밍 요구사항
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 딜러와 플레이어에서 발생하는 중복 코드를 제거해야 한다.
6 changes: 3 additions & 3 deletions src/main/kotlin/blackjack/controller/BlackJackController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object BlackJackController {
val players =
dealer.initPlayers(
fetchPlayerNames = { InputView.readPlayerNames() },
getBettingAmount = { name -> InputView.readBettingAmount(name) },
onPlayerInit = { names ->
ResultView.printPlayerInitMessage(names)
ResultView.printDealerWithCard(dealer.getCardForInitialDisplay())
Expand Down Expand Up @@ -59,8 +60,7 @@ object BlackJackController {
players.onEach { player ->
ResultView.printFinalScoresForPlayer(player)
}

val result = BlackJackResultManager(dealer, players).getResult()
ResultView.printFinalWinLose(result)
val result = BlackJackResultManager(dealer, players).getResultV2()
ResultView.printFinalProfit(result)
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/blackjack/domain/BetMoney.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package blackjack.domain

import java.math.BigDecimal
import java.math.RoundingMode

@JvmInline
value class BetMoney(private val amount: BigDecimal) {
fun getOriginalBetAmount(): BigDecimal {
return amount
}

fun getAmountOnBlackJack(): BigDecimal {
return amount.multiply((1.5).toBigDecimal()).setScale(0, RoundingMode.DOWN)
}

fun getAmountOnBust(): BigDecimal {
return -(amount)
}

fun getAmountOnLose(): BigDecimal {
return -(amount)
}
}
45 changes: 33 additions & 12 deletions src/main/kotlin/blackjack/domain/BlackJackResultManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,52 @@ package blackjack.domain
class BlackJackResultManager(
private val dealer: Dealer,
private val players: Players,
private val playerResultCalculator: PlayerResultCalculator = PlayerResultCalculator(),
) {
@Deprecated("step4에서는 getResultV2 를 사용하세요 - step3 에서만 사용됨")
fun getResult(): BlackJackResult {
val dealerScore = dealer.cardsSum
val playersWinLose =
players.value.associateWith { player ->
playerResultCalculator.calculate(dealerScore, player.cardsSum)
PlayerResultCalculator.calculate(dealerScore, player.cardsSum)
}

val dealerWinCount = playersWinLose.count { it.value == PlayerResult.LOSE }
val dealerLoseCount = playersWinLose.count { it.value == PlayerResult.WIN }
return BlackJackResult(dealerWinCount, dealerLoseCount, PlayerToResultMap(playersWinLose))
return BlackJackResult(PlayerToResultMap(playersWinLose))
}

fun getResultV2(): BlackJackResultV2 {
val playersProfits = players.getPlayersToProfitMoney(
getGameResult = { player -> player.getGameResultWith(dealer) },
onSetPlayerProfitMoney = { profitMoney -> dealer.adjustProfit(profitMoney) }
)
return BlackJackResultV2(dealer.profitMoney, playersProfits)
}
}

data class BlackJackResultV2(
val dealerProfitMoney: ProfitMoney,
val playerToProfit: PlayerToProfitMoney,
)

data class PlayerToProfitMoney(val value: Map<Player, ProfitMoney>)

@Deprecated("deprecated - step3 에서만 사용됨")
data class BlackJackResult(
val dealerWinCount: Int,
val dealerLoseCount: Int,
val playerToResultMap: PlayerToResultMap,
)
) {
val dealerWinCount: Int
get() = playerToResultMap.getPlayerLoseCounts()
val dealerLoseCount: Int
get() = playerToResultMap.getPlayerWinningCounts()
}

@Deprecated("deprecated - step3 에서만 사용됨")
@JvmInline
value class PlayerToResultMap(val value: Map<Player, PlayerResult>)
value class PlayerToResultMap(val value: Map<Player, GameResult>) {
fun getPlayerWinningCounts(): Int {
return value.count { it.value == GameResult.WIN }
}

enum class PlayerResult {
WIN,
LOSE,
fun getPlayerLoseCounts(): Int {
return value.count { it.value == GameResult.LOSE }
}
}
8 changes: 0 additions & 8 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ package blackjack.domain
data class Card(val rank: Rank, val suit: Suit) {
private fun isAce() = rank == Rank.ACE

fun isOverMaxSum(currentCardSum: Int): Boolean {
return if (isAce()) {
currentCardSum + ACE_VALUE_ONE > MAX_SUM && currentCardSum + this.rank.value > MAX_SUM
} else {
currentCardSum + this.rank.value > MAX_SUM
}
}

companion object {
const val ACE_VALUE_ONE = 1
const val MAX_SUM = 21
Expand Down
27 changes: 23 additions & 4 deletions src/main/kotlin/blackjack/domain/Dealer.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
package blackjack.domain

import java.math.BigDecimal

class Dealer(
private val drawCard: () -> Card,
) : Participant(drawCard = drawCard) {
fun initPlayers(
fetchPlayerNames: () -> List<String>,
getBettingAmount: (String) -> BigDecimal,
onPlayerInit: (List<String>) -> Unit,
): Players {
val names = fetchPlayerNames()
val players = names.map { name -> Player(name = name, drawCard = drawCard) }
val nameAndBets = names.associateWith(getBettingAmount)
val players =
Players(
nameAndBets.map { (name, bet) ->
Player(
name = name,
betMoney = BetMoney(bet),
drawCard = drawCard,
)
},
)
onPlayerInit(names)
return Players(value = players)
return players
}

fun drawOneMoreCardIfNeeded(onDrawCard: () -> Unit) {
addCardIfAvailable(requireCard = { drawCard() }, onDrawCard = onDrawCard)
addCardIfAvailable(requireCard = drawCard, onDrawCard = onDrawCard)
}

override fun isAddCardEnabled(): Boolean {
return cardsSum <= 16
return cardsSum <= DEALER_DRAW_ONE_MORE_CARD_THRESHOLD
}

fun getCardForInitialDisplay(): Card {
require(cards.value.isNotEmpty()) { "Dealer should be initialized with $DEALER_CARD_COUNT cards." }
return cards.value[0]
}

fun adjustProfit(playerProfit: ProfitMoney) {
val profit = profitMoney.getCurrentProfit() - playerProfit.getCurrentProfit()
profitMoney.set(profit)
}

companion object {
private const val DEALER_CARD_COUNT = 2
private const val DEALER_DRAW_ONE_MORE_CARD_THRESHOLD = 16
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/blackjack/domain/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blackjack.domain

enum class GameResult {
WIN,
BUST,
LOSE,
PUSH,
BLACK_JACK,
;

companion object {
fun fromScores(
dealerScore: Int,
playerScore: Int,
): GameResult {
return when {
dealerScore > Card.MAX_SUM -> WIN // Dealer bust
playerScore > Card.MAX_SUM -> BUST // Player bust
dealerScore > playerScore -> LOSE
playerScore > dealerScore -> WIN
else -> PUSH
}
}
}
}
10 changes: 8 additions & 2 deletions src/main/kotlin/blackjack/domain/Participant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package blackjack.domain

abstract class Participant(
private val drawCard: () -> Card,
initialCardCount: Int = 2,
) {
private val _cards = mutableListOf<Card>()
val cards: Cards = Cards(_cards)
val cardsSum: Int get() = cards.sumValues()
val profitMoney: ProfitMoney = ProfitMoney()
val isBlackJackInitially: Boolean

init {
repeat(initialCardCount) { addCard(drawCard()) }
repeat(INITIAL_CARD_COUNT) { addCard(drawCard()) }
isBlackJackInitially = cardsSum == Card.MAX_SUM
Comment on lines +9 to +13
Copy link

Choose a reason for hiding this comment

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

여러가지 상태를 관리하는것 같습니다.

Cards를 가지고 점수를 계산하고 이러한 상태가 관리되는것 같은데요

디자인 패턴중 상태패턴을 이용한다면 이러한 부분을 개선해볼 수 있을것 같습니다.

반드시 개선을 바라는 부분은 아니니 참고만 해주셔도 좋습니다. 😄

Copy link
Author

Choose a reason for hiding this comment

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

@pci2676 요거 혹시 간단하게 예시를 주실 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

블랙잭 피드백에 나와있는 내용이군요!
이거 근데 현재 구조에서 적용하려면 코드를 아예 새로 짜야 할 것 같아서
별도로 한번 적용 해보는 연습을 해봐야 할 것 같습니다.

Copy link

Choose a reason for hiding this comment

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

제가 드리는 리뷰를 반드시 반영할 필요는 없으니 참고만 해주셔도 됩니다~

}

private fun addCard(card: Card) {
Expand All @@ -29,4 +31,8 @@ abstract class Participant(
}

abstract fun isAddCardEnabled(): Boolean

companion object {
private const val INITIAL_CARD_COUNT = 2
}
}
42 changes: 35 additions & 7 deletions src/main/kotlin/blackjack/domain/Player.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,53 @@
package blackjack.domain

import java.math.BigDecimal

class Player(
val name: String,
private val betMoney: BetMoney,
private val drawCard: () -> Card,
) : Participant(drawCard = drawCard) {
private lateinit var currentCard: Card

fun play(
isDrawCard: (String) -> Boolean,
onDrawCard: () -> Unit,
onExitPlay: () -> Unit,
) {
while (isDrawCard(name)) {
currentCard = drawCard()
val isCardAdded = addCardIfAvailable(requireCard = { currentCard }, onDrawCard = onDrawCard)
if (isCardAdded.not()) break
var shouldContinue = shouldContinueDrawing(isDrawCard)
while (shouldContinue) {
val isCardAdded = addCardIfAvailable(requireCard = drawCard, onDrawCard = onDrawCard)
shouldContinue = isCardAdded && shouldContinueDrawing(isDrawCard)
}
onExitPlay()
}

fun getGameResultWith(dealer: Dealer): GameResult {
return when {
this.isBlackJackInitially && dealer.isBlackJackInitially.not() -> GameResult.BLACK_JACK
this.isBlackJackInitially && dealer.isBlackJackInitially -> GameResult.PUSH
else -> GameResult.fromScores(dealer.cardsSum, this.cardsSum)
}
}

fun setProfitMoneyFromGameResult(result: GameResult) {
val betMoney = getBetMoneyFromGameResult(result)
profitMoney.set(betMoney)
}
Copy link

Choose a reason for hiding this comment

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

이러한 상태를 변경하는 메서드는 설계하기 굉장히 까다로운 부분인것 같습니다.

가희님은 이 메서드를 의도와 시기에 맞게 잘 호출하여 사용할 수 있지만

다른 개발자는 이 메서드를 적절한 시기에 올바르게 호출하여 사용할 수 있도록 보장할 수 있을까요?

다른 방법으로 result만 구해낸다면 언제 어떻게든 호출할 수 있지 않을까요?

이를 방어할 수 있다면 더욱 견고해질 수 있을것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

안그래도 암시적으로 상태 변경하는 부분이 걸렸는데,
이거 좀 더 고민해보겠습니다

Copy link
Author

Choose a reason for hiding this comment

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

profitMoney 를 Player private 하게 가져가도록 하고 setter 함수를 제거하는 방향으로 수정했습니다.


private fun getBetMoneyFromGameResult(gameResult: GameResult): BigDecimal {
return when (gameResult) {
GameResult.BLACK_JACK -> betMoney.getAmountOnBlackJack()
GameResult.WIN -> betMoney.getOriginalBetAmount()
GameResult.PUSH -> betMoney.getOriginalBetAmount()
GameResult.LOSE -> betMoney.getAmountOnLose()
GameResult.BUST -> betMoney.getAmountOnBust()
}
}
Copy link

Choose a reason for hiding this comment

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

이 부분은 GameResult에 따라 betMoney를 어떻게 다루어야하는지에 대한 전략을 다루는것 같습니다.

이 부분을 GameResult에게 위임해보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

GameResult 가 가져가도록 수정했습니다.


private fun shouldContinueDrawing(isDrawCard: (String) -> Boolean): Boolean {
return isDrawCard(name)
}

override fun isAddCardEnabled(): Boolean {
return currentCard.isOverMaxSum(cardsSum).not()
return cardsSum < Card.MAX_SUM
}
}
14 changes: 8 additions & 6 deletions src/main/kotlin/blackjack/domain/PlayerResultCalculator.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package blackjack.domain

class PlayerResultCalculator {
@Deprecated("deprecated - step3 에서만 사용됨")
Copy link

Choose a reason for hiding this comment

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

step3에서만 사용되었다면 제거해주셔도 좋을것 같습니다. 😄

Copy link
Author

Choose a reason for hiding this comment

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

제거했습니다 :)

object PlayerResultCalculator {
fun calculate(
dealerScore: Int,
playerScore: Int,
): PlayerResult {
): GameResult {
return when {
dealerScore > 21 -> PlayerResult.WIN
playerScore > 21 -> PlayerResult.LOSE
playerScore > dealerScore -> PlayerResult.WIN
else -> PlayerResult.LOSE
dealerScore > Card.MAX_SUM -> GameResult.WIN
playerScore > Card.MAX_SUM -> GameResult.BUST
dealerScore > playerScore -> GameResult.LOSE
playerScore > dealerScore -> GameResult.WIN
else -> GameResult.PUSH
}
}
}
18 changes: 16 additions & 2 deletions src/main/kotlin/blackjack/domain/Players.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@ package blackjack.domain

data class Players(val value: List<Player>) {
fun onEachPreparePlay(action: (Player) -> Unit): Players {
value.forEach { action(it) }
onEach(action)
return this
}

fun onEachStartPlay(action: (Player) -> Unit): Players {
value.forEach { action(it) }
onEach(action)
return this
}

fun onEach(action: (Player) -> Unit): Players {
value.forEach { action(it) }
return this
}

fun getPlayersToProfitMoney(
getGameResult: (Player) -> GameResult,
onSetPlayerProfitMoney: (ProfitMoney) -> Unit
): PlayerToProfitMoney {
val map =
value.associateWith { player ->
val gameResult = getGameResult(player)
player.setProfitMoneyFromGameResult(gameResult)
onSetPlayerProfitMoney(player.profitMoney)
player.profitMoney
}
return PlayerToProfitMoney(map)
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/ProfitMoney.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package blackjack.domain

import java.math.BigDecimal

class ProfitMoney {
private var current: BigDecimal = BigDecimal.ZERO

fun getCurrentProfit(): BigDecimal {
return current
}

fun set(amount: BigDecimal) {
current = amount
}
}
Comment on lines +5 to +15
Copy link

Choose a reason for hiding this comment

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

kotlin을 사용하신다면 가능한 불변한 객체를 설계하여 사용하시는것을 권장드립니다~

https://kotlinlang.org/docs/coding-conventions.html#idiomatic-use-of-language-features

Loading