diff --git a/README.md b/README.md index 50232ec0d..1a8cc9305 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,87 @@ - git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다. ### 기능 목록 -게임 입력 -- [x] 게임에 참여할 사람의 이름을 입력 받는다 +InputView : 게임에 필요한 입력값을 입력 받는다 +- 게임에 참여할 사람의 이름을 입력 받는다 - 쉼표 기준으로 분리한다 - 2명의 이름을 입력 받는다 - 참가자의 이름은 공백일 수 없다. -게임 세팅 -- [x] 입력 받은 사람의 이름으로 참가자를 생성한다 -- [x] 카드 52장 (1덱)을 세팅한다 +- [x] 플레이어는 게임을 시작할 때 베팅 금액을 정해야 한다 + +InputProcessor : 입력 금액을 검증하고 도메인 레벨로 변환한다 +- 게임에 참여할 사람의 이름을 처리한다 + - 게임 참여자는 2명이여야 한다 + - 게임 참여자의 이름은 서로 달라야 한다 + - 게임 참여자 이름은 공백일 수 없다. +- [x] 베팅 금액을 처리한다 + - 베팅 금액은 0보다 커야 한다 + +BlackJackGame : 게임을 진행한다 +- distributor 가 dealEnd 가 될 때까지 카드 배분을 진행한다 +- 각 카드 배분의 결과를 result 처리기에 넘긴다 +- 초기 카드 배분자는 DealIntialCards()이다 +- 카드 배분 횟수를 초과하면 종료시킨다 + - 한 사람이 최대 가질 수 있는 카드 수 11장 ( Ace(1) * 4 + 2 * 4 + 3 * 3) : 배분 9번 진행 + - 한 게임당 가질 수 있는 최대 카드 배분 횟수 : 초키 카드 배분 + 9번 진행 * 2 + 딜러 카드 배분 = 20 + +GameTable +- 딜러와 플레이어를 갖고 상호간의 카드 이동을 담당한다 +- 배팅 보드를 가지고 게임이 끝나면 게임 결과로 베팅 결과를 산출한다 + +Players +- 플레이어를 관리하고 어떤 플레이어의 턴인지 관리한다 + - 플레이어 이름으로 플레이어를 생성한다 + - PlayerNames : 플레이어 이름 리스트 + - [x] 각 플레이어는 다른 이름이어야 함 + +Dealer +- 카드 52장 (1덱)을 세팅한다 - 스페이스, 하트, 다이아, 클로버 4개의 각 문양이 2~10, jack, queen, king, ace 13장으로 구성 -게임 진행 -- 처음 카드 배분 - - [x] 참가자와 딜러에게 각각 2장의 카드를 나눠 준다 - - [x] 받은 카드를 출력한다 - - 딜러의 카드는 한 장만 출력한다 -- 플레이어 카드 배분 - - [x] 참가자 중 1명에게 카드를 더 받을지 묻고, 참가자는 y 또는 n 으로 대답한다. - - [x] 참가자가 y를 입력하면 카드 한 장을 더 지급한다 - - [x] 한 사람이 받은 카드를 계산했을 때 21을 넘는다면 더이상 카드를 받을 수 없다. - - [x] 참가자가 n을 입력하면 다른 참가자에게 턴이 넘어간다. 두번째 참가자라면 딜러에게 턴이 넘어간다 - - [x] 참가자가 y, n을 입력하면 카드 지급 여부에 관계 없이 결과를 출력한다 -- 딜러 카드 배분 - - [x] 처음 받은 2장의 카드의 합이 16이하이면 1장의 카드를 받고, 17이상이면 카드를 받지 않는다 - - [x] 딜러가 카드를 받았는지 여부를 출력한다 -- 결과 출력 - - [x] 게임이 종료되면 딜러와 플레이어의 카드 점수를 출력한다 - - [x] 게임 최종 결과는 21을 넘지 않으면서, 21에 가까운 방식으로 계산한다 - - 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. -- 승패 계산 - - [x] 승패를 가른다 - - 각 플레이어와 딜러의 승패를 가른다 - - 버스트 : 21점 초과 / 푸시 : 딜러와 동점 (무승부) - - 딜러가 각각 딜러와 다 겨뤄 21에 더 가까운 사람이 승리한다 - - 버스트시에는 점수가 0점이다 - - 플레이어와 딜러가 둘 다 버스트라면 딜러가 승리한다 - - 딜러와 플레이어가 버스트가 아니면서 동일 점수라면 무승부 - - 가른 승패의 결과를 출력한다 -- 게임 종료 - - [x] 게임을 종료시킨다 - - 카드 배분이 끝나면 (DistributionEnd) 게임을 종료시킨다 - - 카드 배분 횟수를 초과하면 종료시킨다 - - 한 사람이 최대 가질 수 있는 카드 수 11장 ( Ace(1) * 4 + 2 * 4 + 3 * 3) : 배분 9번 진행 - - 한 게임당 가질 수 있는 최대 카드 배분 횟수 : 초키 카드 배분 + 9번 진행 * 2 + 딜러 카드 배분 = 20 +- 자신의 카드 보유에 따라 hit or stand 를 반환한다 + - 처음 받은 2장의 카드의 합이 16이하이면 1장의 카드를 받고, 17이상이면 카드를 받지 않는다 + +Player +- [x] 플레이어는 이름이 같으면 동일한 플레이어로 취급된다 +- hitOrStand: 사용자의 의견에 따라 카드를 더 받을지(hit) 그만할지(stand)를 반환한다 + - 사용자가 hit 를 요청했더라도 카드 점수가 21점 이상이면 stand 를 반환한다 +- 딜러와의 점수를 비교해서 승패를 가를 수 있다 + - 버스트 : 딜러 점수와 상관 없이 패 + - 그 외 딜러보다 점수가 높으면 승, 같으면 무, 낮으면 패 + +BettingBoard : 베팅 보드 +- [x] 플레이어별 베팅 금액을 저장 +- [x] 게임이 종료되면 플레이어별 승패 결과에 따라 각 플레이어가 받을 금액을 결정한다 + - 플레이어가 이겼을 때 + - 블랙잭으로 이겼을 때 : 베팅금액 회수 + 베팅금액 * 1.5 + - 일반 수로 이겼을 때 : 베팅금액 회수 + 베팅금액 * 1 + - 무승부일 때 : 베팅금액 회수 + - 졌을 때: 베팅금액 뺏김 + - PlayerBet + - 플레이어의 베팅 금액과 받은 금액으로 최종 수익을 계산한다 + +DealInitialCards : 처음 카드 배분 +- 참가자와 딜러에게 각각 2장의 카드를 나눠 준다 + +DealToPlayer +- 참가자 중 1명에게 카드를 더 받을지 묻고, 결과에 따라 카드를 지급한다 + - 참가자가 hit 라면 카드 한 장을 더 지급한다 + - 참가자가 stand 라면 다른 참가자에게 턴이 넘어간다. 두번째 참가자라면 딜러에게 턴이 넘어간다 + - 참가자의 hit or stand 마다 배분 결과를 반환한다 + - 참가자가 stand 였을 때 점수가 21점 이상이라면 시스템으로 인한 stand로 판별한다 + +DealToDealer +- 딜러의 카드 상태에 따라 hit or stand 를 조회하고 결과에 따라 카드를 지급한다 +- 딜러까지 카드 배분이 끝나면 카드 배분을 종료한다 + +ResultProcessor +- 발생한 Result 이벤트에 따라 해당 이벤트를 알맞은 ResultHandler 에 전달한다 +- DealToPlayerResult 인 경우 시스템상의 stand 라면 뷰에 결과를 전달하지 않는다 + +ViewResultProcessor +- 딜러의 카드는 한 장만 출력한다 +- 게임이 종료되면 (GameResult 인 경우) 딜러와 플레이어의 카드 점수를 출력한다 +- 베팅 결과를 최종 수익을 출력한다 + +GameResult +- 게임 최종 결과는 21을 넘지 않으면서, 21에 가까운 방식으로 계산한다 +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. diff --git a/src/main/kotlin/blackjack/controller/InputProcessor.kt b/src/main/kotlin/blackjack/controller/InputProcessor.kt index 55f3fb8ac..93cc9e627 100644 --- a/src/main/kotlin/blackjack/controller/InputProcessor.kt +++ b/src/main/kotlin/blackjack/controller/InputProcessor.kt @@ -1,11 +1,14 @@ package blackjack.controller import blackjack.domain.Action +import blackjack.domain.batting.BetAmount import blackjack.domain.player.Player import blackjack.domain.player.PlayerNames interface InputProcessor { fun playerNames(): PlayerNames + fun playerBet(player: Player): BetAmount + fun playerAction(player: Player): Action } diff --git a/src/main/kotlin/blackjack/controller/ResultProcessor.kt b/src/main/kotlin/blackjack/controller/ResultProcessor.kt index c2ebdb331..584843905 100644 --- a/src/main/kotlin/blackjack/controller/ResultProcessor.kt +++ b/src/main/kotlin/blackjack/controller/ResultProcessor.kt @@ -1,21 +1,20 @@ package blackjack.controller import blackjack.domain.result.Result +import blackjack.domain.result.distribution.DealEndResult import blackjack.domain.result.distribution.DealInitialCardResult import blackjack.domain.result.distribution.DealToDealerResult import blackjack.domain.result.distribution.DealToPlayerResult -import blackjack.domain.result.game.GameResult +import blackjack.domain.result.game.GameEndResult class ResultProcessor { fun handle(result: Result) { when (result) { is DealInitialCardResult -> ViewResultProcessor.drawInitialDistribution(result) - is DealToPlayerResult -> { - if (result.isSystemStand) return - ViewResultProcessor.drawPlayerState(result) - } + is DealToPlayerResult -> ViewResultProcessor.drawPlayerState(result) + is DealEndResult -> {} is DealToDealerResult -> ViewResultProcessor.drawDealerState(result) - is GameResult -> ViewResultProcessor.drawGameResult(result) + is GameEndResult -> ViewResultProcessor.drawGameResult(result) } } } diff --git a/src/main/kotlin/blackjack/controller/ViewInputProcessor.kt b/src/main/kotlin/blackjack/controller/ViewInputProcessor.kt index 6db2bcdc3..92fa684e2 100644 --- a/src/main/kotlin/blackjack/controller/ViewInputProcessor.kt +++ b/src/main/kotlin/blackjack/controller/ViewInputProcessor.kt @@ -1,6 +1,7 @@ package blackjack.controller import blackjack.domain.Action +import blackjack.domain.batting.BetAmount import blackjack.domain.player.Player import blackjack.domain.player.PlayerNames import blackjack.view.dto.PlayerNameDto @@ -8,13 +9,18 @@ import blackjack.view.input.InputView class ViewInputProcessor : InputProcessor { override fun playerNames(): PlayerNames = - InputView.playerNames().let(PlayerNames::from) + PlayerNames.from(InputView.playerNames()) + + override fun playerBet(player: Player): BetAmount = + BetAmount(InputView.playerBet(player.nameDto()).toBigDecimal()) override fun playerAction(player: Player): Action { - val action = InputView.playerAction(player.let(PlayerNameDto::from)) + val action = InputView.playerAction(player.nameDto()) return toPlayerAction(action) } + private fun Player.nameDto(): PlayerNameDto = PlayerNameDto.from(this) + private fun toPlayerAction(action: String): Action = when (action) { "y" -> Action.HIT "n" -> Action.STAND diff --git a/src/main/kotlin/blackjack/controller/ViewResultProcessor.kt b/src/main/kotlin/blackjack/controller/ViewResultProcessor.kt index 6ff37233c..9f80c9139 100644 --- a/src/main/kotlin/blackjack/controller/ViewResultProcessor.kt +++ b/src/main/kotlin/blackjack/controller/ViewResultProcessor.kt @@ -3,11 +3,13 @@ package blackjack.controller import blackjack.domain.result.distribution.DealInitialCardResult import blackjack.domain.result.distribution.DealToDealerResult import blackjack.domain.result.distribution.DealToPlayerResult -import blackjack.domain.result.game.GameResult +import blackjack.domain.result.game.GameEndResult +import blackjack.view.dto.DealerCardsResultDto import blackjack.view.dto.DealerHitDto -import blackjack.view.dto.FinalDealerStateDto -import blackjack.view.dto.FinalPlayerStateDto +import blackjack.view.dto.DealerProfitDto +import blackjack.view.dto.PlayerCardsResultDto import blackjack.view.dto.PlayerDto +import blackjack.view.dto.PlayerProfitDto import blackjack.view.output.OutputView object ViewResultProcessor { @@ -18,6 +20,7 @@ object ViewResultProcessor { } fun drawPlayerState(result: DealToPlayerResult) { + if (result.isSystemStand) return val dto = result.player.let { PlayerDto(it.name.value, it.hand.cards) } OutputView.drawPlayerCurrentState(dto) } @@ -26,15 +29,24 @@ object ViewResultProcessor { OutputView.drawDealerHitStatus(DealerHitDto(result.isHit)) } - fun drawGameResult(result: GameResult) { + fun drawGameResult(result: GameEndResult) { + drawCardResult(result) + drawProfitResult(result) + } + + private fun drawCardResult(result: GameEndResult) { + val dealerDto = DealerCardsResultDto(result.dealerHand.cards, result.dealerScore.value) + val playersDto = result.playerResults + .map { PlayerCardsResultDto(it.name.value, it.hand.cards, it.score.value) } + OutputView.drawCardsResults(dealerDto, playersDto) + } + + private fun drawProfitResult(result: GameEndResult) { val dealerDto = - result.dealerResults.let { - FinalDealerStateDto(it.dealer.hand.cards, it.dealer.score.cardScore, it.status) - } - val playersDto = - result.playersResult.map { - FinalPlayerStateDto(it.player.name.value, it.player.hand.cards, it.player.score.cardScore, it.status) - } - OutputView.drawFinalResults(dealerDto, playersDto) + DealerProfitDto(result.dealerProfit.value.toString()) + val playersDto = result.playerResults.map { + PlayerProfitDto(it.name.value, it.profit.value.toString()) + } + OutputView.drawProfitResults(dealerDto, playersDto) } } diff --git a/src/main/kotlin/blackjack/domain/BlackJackGame.kt b/src/main/kotlin/blackjack/domain/BlackJackGame.kt index 15e12aeb0..ddfc33e44 100644 --- a/src/main/kotlin/blackjack/domain/BlackJackGame.kt +++ b/src/main/kotlin/blackjack/domain/BlackJackGame.kt @@ -2,24 +2,26 @@ package blackjack.domain import blackjack.controller.InputProcessor import blackjack.controller.ResultProcessor +import blackjack.domain.batting.BetBoard import blackjack.domain.distirbution.CardDistributor import blackjack.domain.distirbution.DealEnd import blackjack.domain.distirbution.DealInitialCards import blackjack.domain.player.Players import blackjack.domain.result.Result +import blackjack.domain.result.distribution.DealEndResult +import blackjack.domain.result.game.GameEndResult class BlackJackGame( - private val inputProcessor: InputProcessor, - private val resultProcessor: ResultProcessor = ResultProcessor(), + inputProcessor: InputProcessor, + players: Players = Players.of(inputProcessor.playerNames()) { player -> inputProcessor.playerAction(player) }, + private val resultProcessor: ResultProcessor, ) { - var dealCards: CardDistributor = DealInitialCards( - GameTable( - Dealer(), - Players.of(inputProcessor.playerNames()) { player -> inputProcessor.playerAction(player) } - ), - ) + + var dealCards: CardDistributor = DealInitialCards(GameTable(players)) private set + val betBoard: BetBoard = BetBoard.of(players) { player -> inputProcessor.playerBet(player) } + fun run() { var distributionCount = 0 while (dealCards !is DealEnd && distributionCount < MAX_DISTRIBUTION_COUNT) { @@ -40,8 +42,10 @@ class BlackJackGame( } private fun endDeal() { - val result = dealCards.deal() - emitResult(result) + val dealEndResult = dealCards.deal() as? DealEndResult + ?: throw IllegalArgumentException("배분이 종료되지 않았습니다") + betBoard.closeBetting(dealEndResult) + emitResult(GameEndResult.of(dealEndResult, betBoard)) } companion object { diff --git a/src/main/kotlin/blackjack/domain/BlackJackJudge.kt b/src/main/kotlin/blackjack/domain/BlackJackJudge.kt new file mode 100644 index 000000000..b4fe695b8 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackJackJudge.kt @@ -0,0 +1,23 @@ +package blackjack.domain + +import blackjack.domain.player.CardPlayer +import blackjack.domain.player.Player +import blackjack.domain.result.game.VictoryStatus + +object BlackJackJudge { + private const val BLACK_JACK_CARD_COUNT = 2 + private const val BLACK_JACK_SCORE = 21 + + fun judgeVictory(player: Player, dealer: Dealer): VictoryStatus = + when { + player.isBust() -> VictoryStatus.LOSS + dealer.isBust() || player.score > dealer.score -> VictoryStatus.WIN + player.score == dealer.score -> VictoryStatus.PUSH + else -> VictoryStatus.LOSS + } + + fun isBlackJack(player: Player): Boolean = + (player isSameCardCount BLACK_JACK_CARD_COUNT) && (player isSameScore BLACK_JACK_SCORE) + + private fun CardPlayer.isBust() = score.value > BLACK_JACK_SCORE +} diff --git a/src/main/kotlin/blackjack/domain/GameTable.kt b/src/main/kotlin/blackjack/domain/GameTable.kt index d71ca8c01..34924ea26 100644 --- a/src/main/kotlin/blackjack/domain/GameTable.kt +++ b/src/main/kotlin/blackjack/domain/GameTable.kt @@ -4,8 +4,8 @@ import blackjack.domain.player.Player import blackjack.domain.player.Players data class GameTable( - val dealer: Dealer, val players: Players, + val dealer: Dealer = Dealer(), ) { val isLastPlayerTurn: Boolean get() = players.isLastTurn diff --git a/src/main/kotlin/blackjack/domain/batting/Amount.kt b/src/main/kotlin/blackjack/domain/batting/Amount.kt new file mode 100644 index 000000000..fee55ec48 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/batting/Amount.kt @@ -0,0 +1,24 @@ +package blackjack.domain.batting + +import java.math.BigDecimal + +data class Amount( + val value: BigDecimal, +) { + + init { + require(value >= BigDecimal.ZERO) { "금액은 0이상이어야 합니다" } + } + + operator fun plus(other: Amount): Amount = Amount(value.plus(other.value)) + operator fun times(count: Int): Amount = Amount(value.times(count.toBigDecimal())) + + operator fun times(count: BigDecimal): Amount = + Amount(value * count) + operator fun compareTo(other: Amount): Int = + this.value.compareTo(other.value) + + companion object { + val ZERO = Amount(BigDecimal.ZERO) + } +} diff --git a/src/main/kotlin/blackjack/domain/batting/BetAmount.kt b/src/main/kotlin/blackjack/domain/batting/BetAmount.kt new file mode 100644 index 000000000..0de4190de --- /dev/null +++ b/src/main/kotlin/blackjack/domain/batting/BetAmount.kt @@ -0,0 +1,14 @@ +package blackjack.domain.batting + +import java.math.BigDecimal + +@JvmInline +value class BetAmount( + val value: Amount, +) { + init { + require(value > Amount.ZERO) { "베팅 금액은 0보다 커야 합니다" } + } + + constructor(amount: BigDecimal) : this(Amount(amount)) +} diff --git a/src/main/kotlin/blackjack/domain/batting/BetBoard.kt b/src/main/kotlin/blackjack/domain/batting/BetBoard.kt new file mode 100644 index 000000000..5fa415ced --- /dev/null +++ b/src/main/kotlin/blackjack/domain/batting/BetBoard.kt @@ -0,0 +1,74 @@ +package blackjack.domain.batting + +import blackjack.domain.BlackJackJudge +import blackjack.domain.Dealer +import blackjack.domain.player.Player +import blackjack.domain.player.PlayerName +import blackjack.domain.player.Players +import blackjack.domain.result.distribution.DealEndResult +import blackjack.domain.result.game.Profit +import blackjack.domain.result.game.VictoryStatus +import java.math.BigDecimal + +data class BetBoard( + private val playerBets: MutableMap +) { + fun playerBet(name: PlayerName): PlayerBet = + findPlayerBet(name) + + fun playerProfit(name: PlayerName): Profit = + findFinishedPlayerBet(name).profit + + fun dealerProfit(): Profit = + playerBets.map { + val bet = it.value as? PlayerBet.Finished + ?: throw IllegalStateException("모든 플레이어가 베팅을 끝내지 않았습니다") + bet.profit + }.reduce(Profit::plus).negative + + fun closeBetting(dealEndResult: DealEndResult) { + dealEndResult.players.value.forEach { player -> + closeBet(player, dealEndResult.dealer) + } + } + + private fun closeBet(player: Player, dealer: Dealer) { + val betToFinish = findPlacedPlayerBet(player.name) + val payoutAmount = player.payout(dealer, betToFinish.betAmount) + val finishedBet = PlayerBet.Finished.of(betToFinish, payoutAmount) + playerBets[player.name] = finishedBet + } + + private fun findPlacedPlayerBet(name: PlayerName): PlayerBet.Placed = + findPlayerBet(name) as? PlayerBet.Placed ?: throw IllegalStateException("해당 플레이어의 베팅이 이미 종료되었습니다") + + private fun findFinishedPlayerBet(name: PlayerName): PlayerBet.Finished = + findPlayerBet(name) as? PlayerBet.Finished ?: throw IllegalStateException("해당 플레이어의 베팅이 종료 되지 않았습니다") + + private fun findPlayerBet(name: PlayerName): PlayerBet = + playerBets[name] ?: throw IllegalArgumentException("베팅에 참여한 플레이어가 아닙니다") + + private fun Player.payout(dealer: Dealer, betAmount: BetAmount): Amount = + when (BlackJackJudge.judgeVictory(this, dealer)) { + VictoryStatus.WIN -> calculateWinAmount(this, betAmount) + VictoryStatus.LOSS -> Amount.ZERO + VictoryStatus.PUSH -> betAmount.value + } + + private fun calculateWinAmount(player: Player, betAmount: BetAmount): Amount { + val amount = betAmount.value + return when (BlackJackJudge.isBlackJack(player)) { + true -> amount * BLACK_JACK_MULTIPLIER + amount + false -> amount * 2 + } + } + + companion object { + private val BLACK_JACK_MULTIPLIER = BigDecimal(1.5) + fun of(players: Players, betAmount: (player: Player) -> BetAmount): BetBoard = + BetBoard(players.value.associate { it.name to it.placeBet(betAmount) }.toMutableMap()) + + private fun Player.placeBet(betAmount: (player: Player) -> BetAmount): PlayerBet = + PlayerBet.Placed(this.name, betAmount(this)) + } +} diff --git a/src/main/kotlin/blackjack/domain/batting/PlayerBet.kt b/src/main/kotlin/blackjack/domain/batting/PlayerBet.kt new file mode 100644 index 000000000..4cc3878eb --- /dev/null +++ b/src/main/kotlin/blackjack/domain/batting/PlayerBet.kt @@ -0,0 +1,29 @@ +package blackjack.domain.batting + +import blackjack.domain.player.PlayerName +import blackjack.domain.result.game.Profit + +sealed interface PlayerBet { + val playerName: PlayerName + + data class Placed( + override val playerName: PlayerName, + val betAmount: BetAmount, + ) : PlayerBet + + data class Finished( + override val playerName: PlayerName, + val betAmount: BetAmount, + val payoutAmount: Amount, + ) : PlayerBet { + + val profit: Profit = Profit.of(betAmount.value, payoutAmount) + companion object { + fun of(betPlaced: Placed, payoutAmount: Amount): Finished = Finished( + playerName = betPlaced.playerName, + betAmount = betPlaced.betAmount, + payoutAmount = payoutAmount + ) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/card/Hand.kt b/src/main/kotlin/blackjack/domain/card/Hand.kt index 8ccf5126b..1f5540ea3 100644 --- a/src/main/kotlin/blackjack/domain/card/Hand.kt +++ b/src/main/kotlin/blackjack/domain/card/Hand.kt @@ -6,10 +6,18 @@ data class Hand( val cards: List get() = _cards.toList() + val cardsCount = _cards.size + val ranks: List get() = _cards.map { it.rank } + val isBlackJackCardSize: Boolean = _cards.size == BLACK_JACK_CARD_SIZE + fun add(card: Card) { _cards.add(card) } + + companion object { + private const val BLACK_JACK_CARD_SIZE = 2 + } } diff --git a/src/main/kotlin/blackjack/domain/card/HandScore.kt b/src/main/kotlin/blackjack/domain/card/HandScore.kt index 42a68213b..bdcac177b 100644 --- a/src/main/kotlin/blackjack/domain/card/HandScore.kt +++ b/src/main/kotlin/blackjack/domain/card/HandScore.kt @@ -1,24 +1,15 @@ package blackjack.domain.card -class HandScore( - val cardScore: Int, +data class HandScore( + val value: Int, ) { - val isBust: Boolean - get() = cardScore > BUST_THRESHOLD + operator fun compareTo(other: HandScore): Int = + this.value.compareTo(other.value) - val gameScore: Int - get() { - if (isBust) return BUST_SCORE - return cardScore - } - - infix fun isGreaterCardScoreThan(score: Int) = cardScore > score - infix fun isGreaterGameScoreThan(other: HandScore) = gameScore > other.gameScore - infix fun isSameGameScoreTo(other: HandScore) = gameScore == other.gameScore + val isGreaterOrEqualToMaxScore: Boolean = value >= MAX_SCORE companion object { - private const val BUST_THRESHOLD = 21 - private const val BUST_SCORE = 0 + private const val MAX_SCORE = 21 private const val ACE_BONUS_SCORE = 10 fun from(hand: Hand): HandScore { @@ -26,7 +17,7 @@ class HandScore( val score = ranks.sumOf { it.score() } return when { ranks.hasAce().not() -> score - score + ACE_BONUS_SCORE > BUST_THRESHOLD -> score + score + ACE_BONUS_SCORE > MAX_SCORE -> score else -> score + ACE_BONUS_SCORE }.let(::HandScore) } diff --git a/src/main/kotlin/blackjack/domain/distirbution/CardDistributor.kt b/src/main/kotlin/blackjack/domain/distirbution/CardDistributor.kt index 0c6c9fca4..3469f0f48 100644 --- a/src/main/kotlin/blackjack/domain/distirbution/CardDistributor.kt +++ b/src/main/kotlin/blackjack/domain/distirbution/CardDistributor.kt @@ -7,12 +7,12 @@ abstract class CardDistributor { protected var _nextDistributor: CardDistributor? = null val nextDistributor: CardDistributor get() { - require(_nextDistributor != null) { "아직 배분이 진행 되지 않았습니다" } + requireNotNull(_nextDistributor) { "아직 배분이 진행 되지 않았습니다" } return _nextDistributor as CardDistributor } open fun nextDistributor(): CardDistributor { - require(_nextDistributor != null) { "아직 배분이 진행 되지 않았습니다" } + requireNotNull(_nextDistributor) { "아직 배분이 진행 되지 않았습니다" } return _nextDistributor as CardDistributor } diff --git a/src/main/kotlin/blackjack/domain/distirbution/DealEnd.kt b/src/main/kotlin/blackjack/domain/distirbution/DealEnd.kt index d86d88df0..ba6d1879f 100644 --- a/src/main/kotlin/blackjack/domain/distirbution/DealEnd.kt +++ b/src/main/kotlin/blackjack/domain/distirbution/DealEnd.kt @@ -1,14 +1,14 @@ package blackjack.domain.distirbution import blackjack.domain.GameTable -import blackjack.domain.result.game.GameResult +import blackjack.domain.result.distribution.DealEndResult class DealEnd( override val table: GameTable ) : CardDistributor() { - override fun deal(): GameResult = - GameResult.of(table.players, table.dealer) + override fun deal(): DealEndResult = + DealEndResult(table.players, table.dealer) override fun nextDistributor(): CardDistributor { throw IllegalArgumentException("마지막 카드 배분입니다") diff --git a/src/main/kotlin/blackjack/domain/distirbution/DealToPlayer.kt b/src/main/kotlin/blackjack/domain/distirbution/DealToPlayer.kt index bb7252259..7486a07a8 100644 --- a/src/main/kotlin/blackjack/domain/distirbution/DealToPlayer.kt +++ b/src/main/kotlin/blackjack/domain/distirbution/DealToPlayer.kt @@ -24,15 +24,14 @@ class DealToPlayer( } } - val isSystemStand = (action == Action.STAND && playerInTurn.isBust) + val isSystemStand = (action == Action.STAND && playerInTurn.isGreaterOrEqualToMaxScore) return DealToPlayerResult(playerInTurn, isSystemStand) } private fun endPlayerDistributionIfLastTurn() { - _nextDistributor = when (table.isLastPlayerTurn) { - true -> DealToDealer(table) - false -> DealToPlayer(table) - } + _nextDistributor = + if (table.isLastPlayerTurn) DealToDealer(table) + else DealToPlayer(table) } companion object { diff --git a/src/main/kotlin/blackjack/domain/player/CardPlayer.kt b/src/main/kotlin/blackjack/domain/player/CardPlayer.kt index 16fab5ae8..307949330 100644 --- a/src/main/kotlin/blackjack/domain/player/CardPlayer.kt +++ b/src/main/kotlin/blackjack/domain/player/CardPlayer.kt @@ -11,14 +11,17 @@ interface CardPlayer { val score: HandScore get() = HandScore.from(hand) - val isBust: Boolean - get() = score.isBust + val isGreaterOrEqualToMaxScore: Boolean + get() = score.isGreaterOrEqualToMaxScore - infix fun isGreaterCardScoreThan(other: Int): Boolean = - score isGreaterCardScoreThan other + infix fun isSameCardCount(count: Int): Boolean = hand.cardsCount == count + infix fun isSameScore(score: Int): Boolean = this.score.value == score + + infix fun isGreaterScoreThan(other: Int): Boolean = + score.value > other fun addCard(card: Card) { - require(!isBust) { "버스트라 카드를 더 받을 수 없습니다" } + require(score.isGreaterOrEqualToMaxScore.not()) { "최대 점수 이상이라 카드를 더 받을 수 없습니다" } hand.add(card) } diff --git a/src/main/kotlin/blackjack/domain/player/DealerPlayer.kt b/src/main/kotlin/blackjack/domain/player/DealerPlayer.kt index 573ce15c9..294717d46 100644 --- a/src/main/kotlin/blackjack/domain/player/DealerPlayer.kt +++ b/src/main/kotlin/blackjack/domain/player/DealerPlayer.kt @@ -7,7 +7,7 @@ data class DealerPlayer( override val hand: Hand = Hand(), ) : CardPlayer { override fun hitOrStand(): Action { - if (this isGreaterCardScoreThan HIT_THRESHOLD_SCORE) return Action.STAND + if (this isGreaterScoreThan HIT_THRESHOLD_SCORE) return Action.STAND return Action.HIT } diff --git a/src/main/kotlin/blackjack/domain/player/Player.kt b/src/main/kotlin/blackjack/domain/player/Player.kt index 3e391cf13..00b714612 100644 --- a/src/main/kotlin/blackjack/domain/player/Player.kt +++ b/src/main/kotlin/blackjack/domain/player/Player.kt @@ -5,12 +5,27 @@ import blackjack.domain.card.Hand class Player( val name: PlayerName, - val getDesiredAction: (player: Player) -> Action, + val desiredAction: (player: Player) -> Action, override val hand: Hand = Hand(), ) : CardPlayer { override fun hitOrStand(): Action { - if (isBust) return Action.STAND - return getDesiredAction(this) + if (isGreaterOrEqualToMaxScore) return Action.STAND + return desiredAction(this) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Player + + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + return name.hashCode() } } diff --git a/src/main/kotlin/blackjack/domain/player/PlayerNames.kt b/src/main/kotlin/blackjack/domain/player/PlayerNames.kt index 47b284708..1036bc33c 100644 --- a/src/main/kotlin/blackjack/domain/player/PlayerNames.kt +++ b/src/main/kotlin/blackjack/domain/player/PlayerNames.kt @@ -4,8 +4,12 @@ package blackjack.domain.player value class PlayerNames( val value: List, ) { + init { + require(value.distinct().size == value.size) { "플레이어는 각기 다른 이름이어야 합니다" } + } + companion object { fun from(names: List): PlayerNames = - names.map(::PlayerName).let(::PlayerNames) + PlayerNames(names.map(::PlayerName)) } } diff --git a/src/main/kotlin/blackjack/domain/player/Players.kt b/src/main/kotlin/blackjack/domain/player/Players.kt index 5fa2dd6aa..fca124970 100644 --- a/src/main/kotlin/blackjack/domain/player/Players.kt +++ b/src/main/kotlin/blackjack/domain/player/Players.kt @@ -15,9 +15,6 @@ data class Players( var inTurn: Player = value.first() private set - val isPlayerInTurnOverMaxScore: Boolean - get() = inTurn.isBust - val isLastTurn: Boolean get() = value.indexOf(inTurn) == value.lastIndex @@ -32,10 +29,10 @@ data class Players( private const val PLAYER_COUNT = 2 fun of( names: PlayerNames, - actionOf: (player: Player) -> Action, - ) = names.value.map { name -> createPlayer(name, actionOf) }.let(::Players) + desiredAction: (player: Player) -> Action, + ) = Players(names.value.map { name -> createPlayer(name, desiredAction) }) - private fun createPlayer(name: PlayerName, actionOf: (player: Player) -> Action) = - Player(name, actionOf) + private fun createPlayer(name: PlayerName, desiredAction: (player: Player) -> Action) = + Player(name, desiredAction) } } diff --git a/src/main/kotlin/blackjack/domain/result/distribution/DealEndResult.kt b/src/main/kotlin/blackjack/domain/result/distribution/DealEndResult.kt new file mode 100644 index 000000000..2c9651731 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/result/distribution/DealEndResult.kt @@ -0,0 +1,10 @@ +package blackjack.domain.result.distribution + +import blackjack.domain.Dealer +import blackjack.domain.player.Players +import blackjack.domain.result.Result + +data class DealEndResult( + val players: Players, + val dealer: Dealer, +) : Result() diff --git a/src/main/kotlin/blackjack/domain/result/game/DealerGameResult.kt b/src/main/kotlin/blackjack/domain/result/game/DealerGameResult.kt new file mode 100644 index 000000000..7f23c4d05 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/result/game/DealerGameResult.kt @@ -0,0 +1,19 @@ +package blackjack.domain.result.game + +import blackjack.domain.Dealer +import blackjack.domain.batting.BetBoard +import blackjack.domain.player.DealerPlayer + +data class DealerGameResult( + val dealer: DealerPlayer, + val profit: Profit, +) { + + companion object { + fun of(dealer: Dealer, betBoard: BetBoard): DealerGameResult = + DealerGameResult( + dealer.player, + betBoard.dealerProfit() + ) + } +} diff --git a/src/main/kotlin/blackjack/domain/result/game/DealerResult.kt b/src/main/kotlin/blackjack/domain/result/game/DealerResult.kt deleted file mode 100644 index b93cbc3ec..000000000 --- a/src/main/kotlin/blackjack/domain/result/game/DealerResult.kt +++ /dev/null @@ -1,16 +0,0 @@ -package blackjack.domain.result.game - -import blackjack.domain.Dealer - -data class DealerResult( - val dealer: Dealer, - val status: VictoryStatues, -) { - companion object { - fun of(dealer: Dealer, playerResults: List): DealerResult = - DealerResult( - dealer, - playerResults.map { it.status.opponentResult }.let(::VictoryStatues) - ) - } -} diff --git a/src/main/kotlin/blackjack/domain/result/game/GameEndResult.kt b/src/main/kotlin/blackjack/domain/result/game/GameEndResult.kt new file mode 100644 index 000000000..e15d12a81 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/result/game/GameEndResult.kt @@ -0,0 +1,29 @@ +package blackjack.domain.result.game + +import blackjack.domain.batting.BetBoard +import blackjack.domain.card.Hand +import blackjack.domain.card.HandScore +import blackjack.domain.result.Result +import blackjack.domain.result.distribution.DealEndResult + +data class GameEndResult( + val playerResults: List, + val dealerResult: DealerGameResult, +) : Result() { + + val dealerHand: Hand = dealerResult.dealer.hand + + val dealerScore: HandScore = dealerResult.dealer.score + + val dealerProfit: Profit = dealerResult.profit + + companion object { + fun of(dealEndResult: DealEndResult, betBoard: BetBoard): GameEndResult { + val playerResults = dealEndResult.players.value.map { player -> + PlayerGameResult.of(player, betBoard) + } + val dealerResult = DealerGameResult.of(dealEndResult.dealer, betBoard) + return GameEndResult(playerResults, dealerResult) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/result/game/GameResult.kt b/src/main/kotlin/blackjack/domain/result/game/GameResult.kt deleted file mode 100644 index ef81e0598..000000000 --- a/src/main/kotlin/blackjack/domain/result/game/GameResult.kt +++ /dev/null @@ -1,21 +0,0 @@ -package blackjack.domain.result.game - -import blackjack.domain.Dealer -import blackjack.domain.player.Players -import blackjack.domain.result.Result - -data class GameResult( - val dealerResults: DealerResult, - val playersResult: List, -) : Result() { - - companion object { - fun of(players: Players, dealer: Dealer): GameResult { - val playersResult = players.value.map { PlayerResult.of(it, dealer.player) } - return GameResult( - DealerResult.of(dealer, playersResult), - playersResult - ) - } - } -} diff --git a/src/main/kotlin/blackjack/domain/result/game/PlayerGameResult.kt b/src/main/kotlin/blackjack/domain/result/game/PlayerGameResult.kt new file mode 100644 index 000000000..9ed86b4a7 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/result/game/PlayerGameResult.kt @@ -0,0 +1,27 @@ +package blackjack.domain.result.game + +import blackjack.domain.batting.BetBoard +import blackjack.domain.card.Hand +import blackjack.domain.card.HandScore +import blackjack.domain.player.Player +import blackjack.domain.player.PlayerName + +data class PlayerGameResult( + private val player: Player, + val profit: Profit, +) { + + val name: PlayerName = player.name + + val hand: Hand = player.hand + + val score: HandScore = player.score + + companion object { + fun of(player: Player, betBoard: BetBoard): PlayerGameResult = + PlayerGameResult( + player, + betBoard.playerProfit(player.name) + ) + } +} diff --git a/src/main/kotlin/blackjack/domain/result/game/PlayerResult.kt b/src/main/kotlin/blackjack/domain/result/game/PlayerResult.kt deleted file mode 100644 index 6ba21af13..000000000 --- a/src/main/kotlin/blackjack/domain/result/game/PlayerResult.kt +++ /dev/null @@ -1,23 +0,0 @@ -package blackjack.domain.result.game - -import blackjack.domain.player.DealerPlayer -import blackjack.domain.player.Player - -data class PlayerResult( - val player: Player, - val status: VictoryStatus, -) { - companion object { - fun of(player: Player, dealerPlayer: DealerPlayer): PlayerResult { - val playerScore = player.score - val dealerScore = dealerPlayer.score - val status = when { - playerScore.isBust -> VictoryStatus.LOSS - playerScore isGreaterGameScoreThan dealerScore -> VictoryStatus.WIN - playerScore isSameGameScoreTo dealerScore -> VictoryStatus.PUSH - else -> VictoryStatus.LOSS - } - return PlayerResult(player, status) - } - } -} diff --git a/src/main/kotlin/blackjack/domain/result/game/Profit.kt b/src/main/kotlin/blackjack/domain/result/game/Profit.kt new file mode 100644 index 000000000..7fc59d1d2 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/result/game/Profit.kt @@ -0,0 +1,19 @@ +package blackjack.domain.result.game + +import blackjack.domain.batting.Amount +import java.math.BigDecimal + +data class Profit( + val value: BigDecimal, +) { + + val negative: Profit + get() = Profit(value.negate()) + + operator fun plus(other: Profit): Profit = Profit(value.plus(other.value)) + + companion object { + fun of(betAmount: Amount, payoutAmount: Amount): Profit = + Profit(payoutAmount.value.minus(betAmount.value)) + } +} diff --git a/src/main/kotlin/blackjack/domain/result/game/VictoryStatues.kt b/src/main/kotlin/blackjack/domain/result/game/VictoryStatues.kt deleted file mode 100644 index f4e3c57bd..000000000 --- a/src/main/kotlin/blackjack/domain/result/game/VictoryStatues.kt +++ /dev/null @@ -1,15 +0,0 @@ -package blackjack.domain.result.game - -@JvmInline -value class VictoryStatues( - val value: List, -) { - val winCount: Int - get() = value.count { it == VictoryStatus.WIN } - - val pushCount: Int - get() = value.count { it == VictoryStatus.PUSH } - - val lossCount: Int - get() = value.count { it == VictoryStatus.LOSS } -} diff --git a/src/main/kotlin/blackjack/view/dto/DealerCardsResultDto.kt b/src/main/kotlin/blackjack/view/dto/DealerCardsResultDto.kt new file mode 100644 index 000000000..8d7842ae1 --- /dev/null +++ b/src/main/kotlin/blackjack/view/dto/DealerCardsResultDto.kt @@ -0,0 +1,8 @@ +package blackjack.view.dto + +import blackjack.domain.card.Card + +data class DealerCardsResultDto( + val cards: List, + val score: Int, +) diff --git a/src/main/kotlin/blackjack/view/dto/DealerProfitDto.kt b/src/main/kotlin/blackjack/view/dto/DealerProfitDto.kt new file mode 100644 index 000000000..86ac102d7 --- /dev/null +++ b/src/main/kotlin/blackjack/view/dto/DealerProfitDto.kt @@ -0,0 +1,5 @@ +package blackjack.view.dto + +data class DealerProfitDto( + val profit: String, +) diff --git a/src/main/kotlin/blackjack/view/dto/FinalDealerStateDto.kt b/src/main/kotlin/blackjack/view/dto/FinalDealerStateDto.kt deleted file mode 100644 index 881f4d505..000000000 --- a/src/main/kotlin/blackjack/view/dto/FinalDealerStateDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package blackjack.view.dto - -import blackjack.domain.card.Card -import blackjack.domain.result.game.VictoryStatues - -data class FinalDealerStateDto( - val cards: List, - val cardScore: Int, - val victoryStatus: VictoryStatues, -) diff --git a/src/main/kotlin/blackjack/view/dto/FinalPlayerStateDto.kt b/src/main/kotlin/blackjack/view/dto/FinalPlayerStateDto.kt deleted file mode 100644 index 34b5b6219..000000000 --- a/src/main/kotlin/blackjack/view/dto/FinalPlayerStateDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package blackjack.view.dto - -import blackjack.domain.card.Card -import blackjack.domain.result.game.VictoryStatus - -data class FinalPlayerStateDto( - val name: String, - val cards: List, - val cardScore: Int, - val victoryStatus: VictoryStatus, -) diff --git a/src/main/kotlin/blackjack/view/dto/PlayerCardsResultDto.kt b/src/main/kotlin/blackjack/view/dto/PlayerCardsResultDto.kt new file mode 100644 index 000000000..e7c4e6c16 --- /dev/null +++ b/src/main/kotlin/blackjack/view/dto/PlayerCardsResultDto.kt @@ -0,0 +1,9 @@ +package blackjack.view.dto + +import blackjack.domain.card.Card + +data class PlayerCardsResultDto( + val name: String, + val cards: List, + val score: Int, +) diff --git a/src/main/kotlin/blackjack/view/dto/PlayerProfitDto.kt b/src/main/kotlin/blackjack/view/dto/PlayerProfitDto.kt new file mode 100644 index 000000000..b20af73fb --- /dev/null +++ b/src/main/kotlin/blackjack/view/dto/PlayerProfitDto.kt @@ -0,0 +1,6 @@ +package blackjack.view.dto + +data class PlayerProfitDto( + val name: String, + val profit: String, +) diff --git a/src/main/kotlin/blackjack/view/input/InputView.kt b/src/main/kotlin/blackjack/view/input/InputView.kt index a4e3c2339..d93bbe7ae 100644 --- a/src/main/kotlin/blackjack/view/input/InputView.kt +++ b/src/main/kotlin/blackjack/view/input/InputView.kt @@ -4,6 +4,7 @@ import blackjack.view.dto.PlayerNameDto object InputView { private const val PLAYER_NAMES_MESSAGE = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)" + private const val PLAYER_BET_MESSAGE = "%s의 베팅 금액은?" private const val PLAYER_ACTION_MESSAGE = "%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)" fun playerNames(): List { @@ -16,6 +17,12 @@ object InputView { return readInput() } + fun playerBet(player: PlayerNameDto): String { + println() + println(PLAYER_BET_MESSAGE.format(player.name)) + return readInput() + } + private fun readInput(): String = readlnOrNull() ?: throw IllegalArgumentException("입력 값이 없습니다") } diff --git a/src/main/kotlin/blackjack/view/output/OutputView.kt b/src/main/kotlin/blackjack/view/output/OutputView.kt index a8486ccea..8588cdd3f 100644 --- a/src/main/kotlin/blackjack/view/output/OutputView.kt +++ b/src/main/kotlin/blackjack/view/output/OutputView.kt @@ -1,12 +1,12 @@ package blackjack.view.output import blackjack.domain.card.Card -import blackjack.domain.result.game.VictoryStatues -import blackjack.domain.result.game.VictoryStatus +import blackjack.view.dto.DealerCardsResultDto import blackjack.view.dto.DealerHitDto -import blackjack.view.dto.FinalDealerStateDto -import blackjack.view.dto.FinalPlayerStateDto +import blackjack.view.dto.DealerProfitDto +import blackjack.view.dto.PlayerCardsResultDto import blackjack.view.dto.PlayerDto +import blackjack.view.dto.PlayerProfitDto object OutputView { private const val INITIAL_DISTRIBUTION_MSG = "딜러와 %s에게 2장씩 나누었습니다." @@ -16,9 +16,9 @@ object OutputView { private const val DEALER_STAND_MSG = "딜러는 17이상이라 카드를 받지 않았습니다." private const val DEALER_FINAL_CARDS_MSG = "딜러 카드: %s - 결과: %d" private const val PLAYER_FINAL_CARDS_MSG = "%s카드: %s - 결과: %d" - private const val VICTORY_MSG = "## 최종 승패" - private const val DEALER_VICTORY_MSG = "딜러: %s" - private const val PLAYER_VICTORY_MSG = "%s: %s" + private const val PROFIT_MSG = "## 최종 수익" + private const val DEALER_PROFIT_MSG = "딜러: %s" + private const val PLAYER_PROFIT_MSG = "%s: %s" fun drawInitialDistributionResult( players: List, @@ -49,34 +49,26 @@ object OutputView { } } - fun drawFinalResults( - dealer: FinalDealerStateDto, - players: List, - ) { - drawFinalCards(dealer, players) - drawVictoryStatus(dealer, players) - } - - private fun drawFinalCards( - dealer: FinalDealerStateDto, - players: List, + fun drawCardsResults( + dealer: DealerCardsResultDto, + players: List, ) { println() - println(DEALER_FINAL_CARDS_MSG.format(extractCardsState(dealer.cards), dealer.cardScore)) + println(DEALER_FINAL_CARDS_MSG.format(extractCardsState(dealer.cards), dealer.score)) players.forEach { - println(PLAYER_FINAL_CARDS_MSG.format(it.name, extractCardsState(it.cards), it.cardScore)) + println(PLAYER_FINAL_CARDS_MSG.format(it.name, extractCardsState(it.cards), it.score)) } } - private fun drawVictoryStatus( - dealer: FinalDealerStateDto, - players: List, + fun drawProfitResults( + dealer: DealerProfitDto, + players: List, ) { println() - println(VICTORY_MSG) - println(DEALER_VICTORY_MSG.format(extractVictoryStates(dealer.victoryStatus))) + println(PROFIT_MSG) + println(DEALER_PROFIT_MSG.format(dealer.profit)) players.forEach { - println(PLAYER_VICTORY_MSG.format(it.name, extractVictoryState(it.victoryStatus))) + println(PLAYER_PROFIT_MSG.format(it.name, it.profit)) } } @@ -85,10 +77,4 @@ object OutputView { private fun extractCardsState(cards: List): String = cards.joinToString(", ") { CardView.from(it) } - - private fun extractVictoryStates(states: VictoryStatues): String = - VictoryStatusView.from(states) - - private fun extractVictoryState(state: VictoryStatus): String = - VictoryStatusView.from(state) } diff --git a/src/main/kotlin/blackjack/view/output/VictoryStatusView.kt b/src/main/kotlin/blackjack/view/output/VictoryStatusView.kt deleted file mode 100644 index f4268e394..000000000 --- a/src/main/kotlin/blackjack/view/output/VictoryStatusView.kt +++ /dev/null @@ -1,17 +0,0 @@ -package blackjack.view.output - -import blackjack.domain.result.game.VictoryStatues -import blackjack.domain.result.game.VictoryStatus - -object VictoryStatusView { - fun from(status: VictoryStatus): String = when (status) { - VictoryStatus.WIN -> "승" - VictoryStatus.PUSH -> "무" - VictoryStatus.LOSS -> "패" - } - - fun from(statuses: VictoryStatues): String = - "${statuses.winCount}${from(VictoryStatus.WIN)} " + - "${statuses.pushCount}${from(VictoryStatus.PUSH)} " + - "${statuses.lossCount}${from(VictoryStatus.LOSS)}" -} diff --git a/src/test/kotlin/blackjack/domain/BlackJackGameTest.kt b/src/test/kotlin/blackjack/domain/BlackJackGameTest.kt index 1ed5dac72..1f4887157 100644 --- a/src/test/kotlin/blackjack/domain/BlackJackGameTest.kt +++ b/src/test/kotlin/blackjack/domain/BlackJackGameTest.kt @@ -1,22 +1,24 @@ package blackjack.domain +import blackjack.controller.ResultProcessor import blackjack.domain.player.PlayerName import blackjack.mock.InputProcessorMock import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe class BlackJackGameTest : DescribeSpec({ - describe("게임 생성") { - context("게임에 참여할 2명의 이름 전달") { + describe("BlackJackGame()") { + context("게임에 참여할 2명의 이름 전달하면") { val name1 = "Hong" val name2 = "Kim" val game = BlackJackGame( inputProcessor = InputProcessorMock( playerNames = listOf(name1, name2) ), + resultProcessor = ResultProcessor(), ) - it("전달된 이름으로 플레이어 세팅") { + it("전달된 이름으로 플레이어가 세팅된다") { val playerNames = game.dealCards.table.players.value.map { it.name } playerNames[0] shouldBe PlayerName(name1) diff --git a/src/test/kotlin/blackjack/domain/BlackJackJudgeTest.kt b/src/test/kotlin/blackjack/domain/BlackJackJudgeTest.kt new file mode 100644 index 000000000..6e98948eb --- /dev/null +++ b/src/test/kotlin/blackjack/domain/BlackJackJudgeTest.kt @@ -0,0 +1,123 @@ +package blackjack.domain + +import blackjack.domain.card.Rank +import blackjack.domain.player.DealerPlayer +import blackjack.domain.result.game.VictoryStatus +import blackjack.mock.card +import blackjack.mock.hand +import blackjack.mock.player +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +class BlackJackJudgeTest : DescribeSpec({ + describe("judgeVictory") { + val score22Cards = + hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TWO)) + val blackJackCards = + hand(card(Rank.TEN), card(Rank.ACE)) + val score21NotBlackJackCards = + hand(card(Rank.TEN), card(Rank.FIVE), card(Rank.SIX)) + val score10Cards = hand(card(Rank.FIVE), card(Rank.FIVE)) + val score5Cards = hand(card(Rank.TWO), card(Rank.THREE)) + + context("플레이어가 버스트라면") { + val player = player(hand = score22Cards) + val dealer = Dealer(player = DealerPlayer(score10Cards)) + + it("LOSS다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.LOSS + } + } + context("플레이어가 버스트 이고 딜러도 버스트라면") { + val player = player(hand = score22Cards) + val dealer = Dealer(player = DealerPlayer(hand = score22Cards)) + + it("LOSS다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.LOSS + } + } + context("플레이어(5) < 딜러(10) 로 플레이어가 딜러보다 점수가 낮다면") { + val player = player(hand = score5Cards) + val dealer = Dealer(player = DealerPlayer(score10Cards)) + + it("LOSS다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.LOSS + } + } + context("플레이어(21, 블랙잭) > 딜러(5) 로 플레이어가 블랙잭으로 딜러보다 점수가 높다면") { + val player = player(hand = blackJackCards) + val dealer = Dealer(player = DealerPlayer(score5Cards)) + + it("WIN이다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.WIN + } + } + context("플레이어(10) > 딜러(5) 로 플레이어가 딜러보다 점수가 높다면") { + val player = player(hand = score10Cards) + val dealer = Dealer(player = DealerPlayer(score5Cards)) + + it("WIN이다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.WIN + } + } + + context("플레이어(10)이고 딜러가 버스트라면") { + val player = player(hand = score10Cards) + val dealer = Dealer(player = DealerPlayer(score22Cards)) + + it("WIN이다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.WIN + } + } + + context("플레이어(10) == 딜러(10)로 점수가 같다면") { + val player = player(hand = score10Cards) + val dealer = Dealer(player = DealerPlayer(score10Cards)) + + it("PUSH다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.PUSH + } + } + + context("플레이어(블랙잭) == 딜러(블랙잭)로 점수가 같다면") { + val player = player(hand = blackJackCards) + val dealer = Dealer(player = DealerPlayer(blackJackCards)) + + it("PUSH다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.PUSH + } + } + + context("플레이어(21) == 딜러(블랙잭)로 점수가 같다면") { + val player = player(hand = score21NotBlackJackCards) + val dealer = Dealer(player = DealerPlayer(blackJackCards)) + + it("PUSH다") { + BlackJackJudge.judgeVictory(player, dealer) shouldBe VictoryStatus.PUSH + } + } + } + + describe("isBlackJack") { + context("플레이어가 블랙잭이면(카드가 2장이며, 해당 카드 합이 21)") { + val player = player(hand = hand(card(Rank.TEN), card(Rank.ACE))) + it("true가 반환된다") { + BlackJackJudge.isBlackJack(player) shouldBe true + } + } + + context("플레이어 카드가 2장이지만 21점이 아니면") { + val player = player(hand = hand(card(Rank.TEN), card(Rank.TWO))) + it("false가 반환된다") { + BlackJackJudge.isBlackJack(player) shouldBe false + } + } + + context("플레이어 카드 합이 21이지만 카드 수가 2장 초과면") { + val player = player(hand = hand(card(Rank.ACE), card(Rank.TEN), card(Rank.TEN))) + it("false가 반환된다") { + BlackJackJudge.isBlackJack(player) shouldBe false + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/DealerTest.kt b/src/test/kotlin/blackjack/domain/DealerTest.kt index 3cbb27ff0..72a882957 100644 --- a/src/test/kotlin/blackjack/domain/DealerTest.kt +++ b/src/test/kotlin/blackjack/domain/DealerTest.kt @@ -15,7 +15,7 @@ import io.kotest.matchers.shouldBe class DealerTest : DescribeSpec({ describe("dealCards") { val count = 2 - context("플레이어 1명에게 ${count}장 카드 배분") { + context("플레이어 1명에게 ${count}장 카드를 배분하면") { val cards = listOf( Card(Suit.CLUB, Rank.ACE), Card(Suit.CLUB, Rank.TWO), @@ -27,16 +27,16 @@ class DealerTest : DescribeSpec({ dealer.dealCards(count, player) - it("플레이어에게 카드 전달") { + it("플레이어에게 카드가 전달된다") { player.hand.cards shouldBe listOf(cards[3], cards[2]) } - it("덱에서 카드에서 제거") { + it("덱에서 카드가 제거된다") { dealer.deck.cards shouldBe listOf(cards[0], cards[1]) } } - context("플레이어 2명에게 ${count}장씩 카드 배분") { + context("플레이어 2명에게 ${count}장씩 카드 배분하면") { val deckCards = deck( Card(Suit.CLUB, Rank.ACE), Card(Suit.CLUB, Rank.TWO), @@ -53,12 +53,12 @@ class DealerTest : DescribeSpec({ dealer.dealCards(count, *players.toTypedArray()) - it("플레이어에게 카드 전달") { + it("플레이어에게 해당 카드가 전달된다") { players[0].hand.cards.size shouldBe 2 players[1].hand.cards.size shouldBe 2 } - it("덱에서 카드에서 제거") { + it("덱에서 카드는 제거된다") { dealer.deck.cards.size shouldBe 2 } } @@ -74,14 +74,14 @@ class DealerTest : DescribeSpec({ val dealer = Dealer(deck(cards)) val count = 2 - context("자신에게 ${count}장 카드 배분") { + context("딜러가 자신에게 ${count}장 카드를 배분하면") { dealer dealToSelf count - it("플레이어에게 카드 전달") { + it("플레이어에게 해당 카드기 전달된다") { dealer.hand.cards shouldBe listOf(cards[3], cards[2]) } - it("덱에서 카드 제거") { + it("덱에서 카드가 제거된다") { dealer.deck.cards shouldBe listOf(cards[0], cards[1]) } } @@ -91,36 +91,36 @@ class DealerTest : DescribeSpec({ val dealer = Dealer( player = DealerPlayer(hand(card(Rank.ACE), card(Rank.QUEEN))) ) - context("딜러가 가진 카드의 점수 조회") { + context("딜러가 가진 카드의 점수를 조회하면") { val result = dealer.score - it("계산된 점수 반환") { - result.cardScore shouldBe 21 + it("계산된 점수가 반환된다") { + result.value shouldBe 21 } } } - describe("isGreaterCardScoreThan") { + describe("isGreaterScoreThan") { val score20cards = hand(card(Rank.QUEEN), card(Rank.QUEEN)) val dealer = DealerPlayer(score20cards) context("딜러보다 낮은 점수로 비교하면") { - val result = dealer isGreaterCardScoreThan 16 + val result = dealer isGreaterScoreThan 16 - it("참을 반환") { + it("참을 반환한다") { result shouldBe true } } context("딜러보다 높은 점수로 비교하면") { - val result = dealer isGreaterCardScoreThan 21 + val result = dealer isGreaterScoreThan 21 - it("거짓을 반환") { + it("거짓을 반환한다") { result shouldBe false } } context("딜러와 같은 점수로 비교하면") { - val result = dealer isGreaterCardScoreThan 20 + val result = dealer isGreaterScoreThan 20 - it("거짓을 반환") { + it("거짓을 반환한다") { result shouldBe false } } diff --git a/src/test/kotlin/blackjack/domain/GameTableTest.kt b/src/test/kotlin/blackjack/domain/GameTableTest.kt index 9e8edc842..47edd9655 100644 --- a/src/test/kotlin/blackjack/domain/GameTableTest.kt +++ b/src/test/kotlin/blackjack/domain/GameTableTest.kt @@ -16,14 +16,14 @@ import io.kotest.matchers.shouldBe class GameTableTest : DescribeSpec({ describe("dealToAll") { - context("카드가 모두 한 장도 없는 상태일 때 모두에게 2장씩 지급") { + context("카드가 모두 한 장도 없는 상태일 때 모두에게 2장씩 지급하면") { val dealer = Dealer() val players = players(player(hand = Hand()), player(hand = Hand())) - val table = GameTable(dealer, players) + val table = GameTable(players, dealer) table.dealToAll(2) - it("모두 두장씩 수령") { + it("모두 두장씩 수령한다") { table.dealer.hand.cards.size shouldBe 2 table.players.value.forEach { player -> player.hand.cards.size shouldBe 2 @@ -36,19 +36,19 @@ class GameTableTest : DescribeSpec({ val dealer = Dealer() val deckCount = dealer.deck.cards.size val players = players(player(hand = Hand()), player()) - val table = GameTable(dealer, players) + val table = GameTable(players, dealer) - context("카드 1장 배분") { + context("현재 플레이어에게 카드 1장을 배분하면") { val playerInTurn = players.inTurn playerInTurn.hand.cards.size shouldBe 0 table.dealToPlayerInTurn(1) - it("차례인 플레이어는 카드 1장 수령") { + it("차례인 플레이어는 카드 1장을 수령한다") { playerInTurn.hand.cards.count() shouldBe 1 } - it("덱에서는 카드 1장 제거") { + it("덱에서는 카드 1장이 제거된다") { dealer.deck.cards.size shouldBe deckCount - 1 } } @@ -56,14 +56,14 @@ class GameTableTest : DescribeSpec({ describe("passPlayerTurnIfNotLastTurn") { val players = players(player("kim"), player("lee")) - val table = GameTable(Dealer(), players) + val table = GameTable(players) context("플레이어 1이 차례인 경우") { players.inTurn shouldBe players.value.first() table.passPlayerTurnIfNotLastTurn() - it("플레이어 2에게 차례가 넘어감") { + it("플레이어 2에게 차례가 넘어간다") { players.inTurn shouldBe players.value.last() } } @@ -86,19 +86,19 @@ class GameTableTest : DescribeSpec({ ) val dealer = Dealer(deck(cards)) val deckCount = dealer.deck.cards.size - val table = GameTable(dealer, players()) + val table = GameTable(players(), dealer) val count = 2 - context("딜러 카드가 한 장도 없을 때 카드 ${count}장 배분") { + context("딜러 카드가 한 장도 없을 때 카드 ${count}장씩 배분하면") { dealer.hand.cards.size shouldBe 0 table.dealToDealer(2) - it("딜러는 ${count}장 카드 수령") { + it("딜러는 ${count}장의 카드를 수령한다") { dealer.hand.cards.size shouldBe count } - it("덱에서는 카드 ${count}장 제거") { + it("덱에서는 카드 ${count}장이 제거된다") { dealer.deck.cards.size shouldBe deckCount - count } } @@ -106,19 +106,19 @@ class GameTableTest : DescribeSpec({ describe("isLastPlayerTurn") { val players = listOf(player("kim"), player("lee")) - val table = GameTable(Dealer(), Players(players)) - context("첫 번째 플레이어 턴") { + val table = GameTable(Players(players)) + context("첫 번째 플레이어 턴이라면") { table.players.inTurn shouldBe players.first() - it("false 반환") { + it("false가 반환된다") { table.isLastPlayerTurn shouldBe false } } - context("마지막 플레이어 턴") { + context("마지막 플레이어 턴이라면") { table.passPlayerTurnIfNotLastTurn() table.players.inTurn shouldBe players.last() - it("true 반환") { + it("true가 반환된다") { table.isLastPlayerTurn shouldBe true } } @@ -126,45 +126,45 @@ class GameTableTest : DescribeSpec({ describe("playerInTurn") { val players = listOf(player("kim"), player("lee")) - val table = GameTable(Dealer(), Players(players)) - context("첫 번쨰 플레이어 턴") { + val table = GameTable(Players(players)) + context("첫 번쨰 플레이어 턴이라면") { table.players.inTurn shouldBe players.first() - it("첫 번째 플레이어 반환") { + it("첫 번째 플레이어 반환된다") { table.playerInTurn shouldBe players.first() } } - context("마지막 플레이어 턴") { + context("마지막 플레이어 턴이라면") { table.passPlayerTurnIfNotLastTurn() table.players.inTurn shouldBe players.last() - it("두 번째 플레이어 반환") { + it("두 번째 플레이어 반환된다") { table.playerInTurn shouldBe players.last() } } } describe("playerInTurnAction") { - context("이번 턴의 플레이어가 HIT를 하면(player: HIT, 점수: 21이하)") { + context("이번 턴의 플레이어가 HIT를 하면(player: HIT, 점수: 21미만)") { val hitPlayer = player(action = Action.HIT, hand = Hand()) val players = players(hitPlayer, player("other")) - val table = GameTable(Dealer(), players) + val table = GameTable(players) table.playerInTurn shouldBe hitPlayer - it("HIT 반환") { + it("HIT가 반환된다") { val result = table.playerInTurnAction result shouldBe Action.HIT } } - context("이번 턴의 플레이어가 STAND를 하면(player: STAND, 점수: 21이하)") { + context("이번 턴의 플레이어가 STAND를 하면(player: STAND, 점수: 21미만)") { val standPlayer = player(action = Action.STAND, hand = Hand()) val players = players(standPlayer, player("other")) - val table = GameTable(Dealer(), players) + val table = GameTable(players) table.playerInTurn shouldBe standPlayer - it("STAND 반환") { + it("STAND가 반환된다") { val result = table.playerInTurnAction result shouldBe Action.STAND @@ -172,12 +172,12 @@ class GameTableTest : DescribeSpec({ } context("이번 턴의 플레이어가 STAND를 하면(player: HIT, 점수: 21이상)") { - val standPlayer = player(action = Action.HIT, hand = hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN))) + val standPlayer = player(action = Action.HIT, hand = hand(card(Rank.TEN), card(Rank.ACE))) val players = players(standPlayer, player("other")) - val table = GameTable(Dealer(), players) + val table = GameTable(players) table.playerInTurn shouldBe standPlayer - it("STAND 반환") { + it("STAND가 반환된다") { val result = table.playerInTurnAction result shouldBe Action.STAND @@ -188,10 +188,10 @@ class GameTableTest : DescribeSpec({ describe("dealerAction") { context("딜러가 HIT를 하면 (딜러 16점 이하)") { val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TWO), card(Rank.THREE)))) - val table = GameTable(dealer, players()) + val table = GameTable(players(), dealer) dealer.hitOrStand() shouldBe Action.HIT - it("HIT 반환") { + it("HIT가 반환된다") { val result = table.dealerAction result shouldBe Action.HIT @@ -200,10 +200,10 @@ class GameTableTest : DescribeSpec({ context("딜러가 STAND를 하면 (딜러 17점 이상)") { val dealer = Dealer(player = DealerPlayer(hand(card(Rank.QUEEN), card(Rank.QUEEN)))) - val table = GameTable(dealer, players()) + val table = GameTable(players(), dealer) dealer.hitOrStand() shouldBe Action.STAND - it("STAND 반환") { + it("STAND가 반환된다") { val result = table.dealerAction result shouldBe Action.STAND diff --git a/src/test/kotlin/blackjack/domain/betting/AmountTest.kt b/src/test/kotlin/blackjack/domain/betting/AmountTest.kt new file mode 100644 index 000000000..c63291604 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/betting/AmountTest.kt @@ -0,0 +1,44 @@ +package blackjack.domain.betting + +import blackjack.domain.batting.Amount +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import java.lang.IllegalArgumentException +import java.math.BigDecimal + +class AmountTest : StringSpec({ + "0이상의 금액이 생성된다" { + val amount = BigDecimal.valueOf(3_000) + + val result = Amount(amount) + + result.value shouldBe amount + } + + "0미만의 숫자는 금액이 될 수 없다" { + val amount = -1.toBigDecimal() + + shouldThrowExactly { + Amount(amount) + } + } + + "금액 끼리는 더할수 있다" { + val result = Amount(BigDecimal(3_000)) + Amount(BigDecimal(5000)) + + result shouldBe Amount(BigDecimal(8_000)) + } + + "금액의 배수(Int)를 구할수 있다" { + val result = Amount(BigDecimal(3_000)) * 3 + + result shouldBe Amount(BigDecimal(9_000)) + } + + "금액의 곱(BigDecimal)을 구할수 있다" { + val result = Amount(BigDecimal(3_000)) * BigDecimal(3) + + result shouldBe Amount(BigDecimal(9_000)) + } +}) diff --git a/src/test/kotlin/blackjack/domain/betting/BetBoardTest.kt b/src/test/kotlin/blackjack/domain/betting/BetBoardTest.kt new file mode 100644 index 000000000..794866699 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/betting/BetBoardTest.kt @@ -0,0 +1,236 @@ +package blackjack.domain.betting + +import blackjack.domain.Dealer +import blackjack.domain.batting.BetAmount +import blackjack.domain.batting.BetBoard +import blackjack.domain.batting.PlayerBet +import blackjack.domain.card.Rank +import blackjack.domain.player.DealerPlayer +import blackjack.domain.player.Player +import blackjack.domain.player.PlayerName +import blackjack.domain.result.distribution.DealEndResult +import blackjack.domain.result.game.Profit +import blackjack.mock.amount +import blackjack.mock.card +import blackjack.mock.hand +import blackjack.mock.player +import blackjack.mock.players +import blackjack.mock.profit +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import java.math.BigDecimal + +class BetBoardTest : DescribeSpec({ + describe("BettingBoard") { + context("플레이어 이름으로 베팅 금액 (kim: 3_000원, lee: 4_000원)이 등록되면") { + val players = players(player("kim"), player("lee")) + val kimAmount = BetAmount(amount(3_000)) + val leeAmount = BetAmount(amount(4_000)) + val betAmount = { player: Player -> + if (player.name.value == "kim") kimAmount + else leeAmount + } + + val betBoard: BetBoard = BetBoard.of(players, betAmount) + + it("플레이어 kim 의 베팅 금액은 3_000이 된다") { + val name = PlayerName("kim") + val playerBet = betBoard.playerBet(name) + + playerBet.playerName shouldBe name + (playerBet as? PlayerBet.Placed)?.betAmount shouldBe kimAmount + } + + it("플레이어 lee 의 베팅 금액은 4_000이 된다") { + val name = PlayerName("lee") + val playerBet = betBoard.playerBet(name) + + playerBet.playerName shouldBe name + (playerBet as? PlayerBet.Placed)?.betAmount shouldBe leeAmount + } + } + } + + describe("playerProfit") { + context("플레이어가 (베팅: 1000, 받은돈: 0) 으로 베팅을 완료했을 때") { + val name = PlayerName("kim") + val betAmount = BetAmount(amount(1000)) + val payoutAmount = amount(0) + val finishedBet = PlayerBet.Finished(name, betAmount, payoutAmount) + val betBord = BetBoard( + mutableMapOf( + name to finishedBet, + PlayerName("lee") to PlayerBet.Finished(PlayerName("lee"), BetAmount(amount(5_000)), amount(5_000)) + ) + ) + + val result = betBord.playerProfit(name) + + it("베팅 수익은 -1000이 조회된다") { + result.value shouldBe BigDecimal(-1000) + } + } + + context("베팅이 완료되지 않은 플레이어의 수익을 조회하면") { + val name = PlayerName("kim") + val betAmount = BetAmount(amount(1000)) + val placedBet = PlayerBet.Placed(name, betAmount) + val betBord = BetBoard( + mutableMapOf( + name to placedBet, + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + it("수익 조회에 실패한다") { + shouldThrowExactly { + betBord.playerProfit(name) + } + } + } + + describe("dealerProfit") { + context("플레이어1의 수익이 2000, 플레이어2의 수익이 -1000일 때") { + val betBoard = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Finished(PlayerName("kim"), BetAmount(amount(2_000)), amount(4_000)), + PlayerName("lee") to PlayerBet.Finished(PlayerName("lee"), BetAmount(amount(1_000)), amount(0)), + ) + ) + + betBoard.playerProfit(PlayerName("kim")) shouldBe profit(2_000) + betBoard.playerProfit(PlayerName("lee")) shouldBe profit(-1000) + + it("딜러의 수익은 두 플레이어 수익의 합인 (-1000)이 된다") { + betBoard.dealerProfit() shouldBe profit(-1000) + } + } + context("모든 플레이어 베팅이 끝나지 않았다면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + it("수익 조회에 실패한다") { + shouldThrowExactly { + betBord.dealerProfit() + } + } + } + } + + describe("closeBetting") { + context("딜러를 블랙잭으로 이긴 플레이어라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.ACE), card(Rank.TEN))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TEN), card(Rank.TEN)))) + + it("베팅 종료시 수익이 베팅 금액의 1.5배가 된다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + val expect = (5_000 * 1.5).toBigDecimal() + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(expect) + } + } + + context("딜러를 일반 숫자로 이긴 플레이어라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.TEN), card(Rank.TEN))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN)))) + + it("베팅 종료시 수익이 1배가 된다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + val expect = (5_000 * 1).toBigDecimal() + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(expect) + } + } + + context("딜러와 플레이어 둘 다 블랙잭으로 무승부라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.TEN), card(Rank.ACE))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TEN), card(Rank.ACE)))) + + it("베팅 종료시 수익은 0이다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(BigDecimal(0)) + } + } + + context("딜러와 둘다 일반 점수로 무승부라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.TWO), card(Rank.ACE))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TWO), card(Rank.ACE)))) + + it("베팅 종료시 수익은 0이다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(BigDecimal(0)) + } + } + + context("플레이어의 카드가 버스트라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TWO), card(Rank.ACE)))) + + it("베팅 종료시 수익은 -베팅금액 이다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(BigDecimal(-5000)) + } + } + + context("플레이어도 딜러도 모두 버스트라면") { + val betBord = BetBoard( + mutableMapOf( + PlayerName("kim") to PlayerBet.Placed(PlayerName("kim"), BetAmount(amount(5_000))), + PlayerName("lee") to PlayerBet.Placed(PlayerName("lee"), BetAmount(amount(5_000))) + ) + ) + + val player = player("kim", hand = hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN))) + val dealer = Dealer(player = DealerPlayer(hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN)))) + + it("베팅 종료시 수익은 -베팅금액 이다") { + betBord.closeBetting(DealEndResult(players(player, player("lee")), dealer)) + + betBord.playerProfit(PlayerName("kim")) shouldBe Profit(BigDecimal(-5000)) + } + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/betting/PlayerBetTest.kt b/src/test/kotlin/blackjack/domain/betting/PlayerBetTest.kt new file mode 100644 index 000000000..9a15ddae6 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/betting/PlayerBetTest.kt @@ -0,0 +1,33 @@ +package blackjack.domain.betting + +import blackjack.domain.batting.BetAmount +import blackjack.domain.batting.PlayerBet +import blackjack.domain.player.PlayerName +import blackjack.mock.amount +import blackjack.mock.profit +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class PlayerBetTest : StringSpec({ + "BetPlaced와 수익 정보로 BetFinished 생성" { + val placed = PlayerBet.Placed( + playerName = PlayerName("kim"), + betAmount = BetAmount(amount(3_000)) + ) + val payoutAmount = amount(0) + + val result = PlayerBet.Finished.of(placed, payoutAmount) + + result.playerName shouldBe placed.playerName + result.betAmount shouldBe placed.betAmount + result.payoutAmount shouldBe payoutAmount + } + + "BetFinished의 수익 조회" { + val betAmount = BetAmount(amount(10000)) + val payoutAmount = amount(25000) + val bet = PlayerBet.Finished(PlayerName("lee"), betAmount, payoutAmount) + + bet.profit shouldBe profit(15000) + } +}) diff --git a/src/test/kotlin/blackjack/domain/card/CardTest.kt b/src/test/kotlin/blackjack/domain/card/CardTest.kt index 025f9ab88..53537279c 100644 --- a/src/test/kotlin/blackjack/domain/card/CardTest.kt +++ b/src/test/kotlin/blackjack/domain/card/CardTest.kt @@ -5,33 +5,33 @@ import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe class CardTest : DescribeSpec({ - describe("카드 생성") { - context("카드 전체 요청") { + describe("카드 생성(allShuffled)") { + context("카드 전체를 요청하면") { val result = Card.allShuffled() - it("카드 전체는 52장") { + it("카드 전체는 52장이다") { result.cards.size shouldBe 52 } - it("스페이드 카드는 모든 랭크에 대한 13장") { + it("클럽 카드는 모든 랭크마다 있으며 총 13장이다") { val spadeCards = result.cards.filter { it.suit == Suit.CLUB } spadeCards.size shouldBe 13 spadeCards.map { it.rank } shouldContainAll Rank.entries } - it("다이아몬드 카드 13장") { + it("다이아몬드 카드는 모든 랭크마다 있으며 총 13장이다") { val diamondCards = result.cards.filter { it.suit == Suit.DIAMOND } diamondCards.size shouldBe 13 diamondCards.map { it.rank } shouldContainAll Rank.entries } - it("하트 카드 13장") { + it("하트 카드는 모든 랭크마다 있으며 총 13장이다") { val heartCard = result.cards.filter { it.suit == Suit.HEART } heartCard.size shouldBe 13 heartCard.map { it.rank } shouldContainAll Rank.entries } - it("스페이드 카드 13장") { + it("스페이드 카드는 모든 랭크마다 있으며 총 13장이다") { val spadeCard = result.cards.filter { it.suit == Suit.SPADE } spadeCard.size shouldBe 13 spadeCard.map { it.rank } shouldContainAll Rank.entries diff --git a/src/test/kotlin/blackjack/domain/card/DeckTest.kt b/src/test/kotlin/blackjack/domain/card/DeckTest.kt index 7264d4011..caef3991e 100644 --- a/src/test/kotlin/blackjack/domain/card/DeckTest.kt +++ b/src/test/kotlin/blackjack/domain/card/DeckTest.kt @@ -6,29 +6,29 @@ import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe class DeckTest : DescribeSpec({ - describe("카드 리스트로 덱 생성") { + describe("Deck.from()") { context("카드 리스트로") { val cards = listOf(Card(Suit.CLUB, Rank.SEVEN), Card(Suit.DIAMOND, Rank.FOUR)) - it("덱 생성") { + it("덱을 생성한다") { val result = Deck.from(cards) result.cards shouldBe cards } } } - describe("덱에서 카드 제거") { - context("덱에서 카드를 한 장 빼면") { + describe("draw()") { + context("덱에서 카드를 한 장을 빼면") { val cards = listOf(Card(Suit.HEART, Rank.FOUR), Card(Suit.CLUB, Rank.SEVEN)) val deck = deck(cards) val result = deck.draw() - it("맨 뒤의 카드 가져옴") { + it("맨 뒤의 카드 가져온다") { result shouldBe Card(Suit.CLUB, Rank.SEVEN) } - it("덱에서는 해당 카드 제거") { + it("덱에서는 해당 카드 제거한다") { deck.cards shouldBe listOf(Card(Suit.HEART, Rank.FOUR)) } } @@ -36,7 +36,7 @@ class DeckTest : DescribeSpec({ context("덱에 카드가 없으면") { val deck = Deck(ArrayDeque()) - it("카드 가져오기 실패") { + it("카드 한장 빼기에 실패한다") { shouldThrowExactly { deck.draw() } diff --git a/src/test/kotlin/blackjack/domain/card/HandScoreTest.kt b/src/test/kotlin/blackjack/domain/card/HandScoreTest.kt index 9901edc47..fb54c665c 100644 --- a/src/test/kotlin/blackjack/domain/card/HandScoreTest.kt +++ b/src/test/kotlin/blackjack/domain/card/HandScoreTest.kt @@ -8,14 +8,14 @@ import io.kotest.data.row import io.kotest.matchers.shouldBe class HandScoreTest : DescribeSpec({ - describe("cardScore") { + describe("HandScore.from(Hand)") { context("Ace를 1점으로 계산했을 때 합이 11이하라면") { forAll( row(hand(card(Rank.TWO), card(Rank.ACE)), 13), row(hand(card(Rank.TWO), card(Rank.THREE), card(Rank.FIVE), card(Rank.ACE)), 21), ) { hand, expect -> it("Ace 1개를 11점으로 계산한다") { - HandScore.from(hand).cardScore shouldBe expect + HandScore.from(hand).value shouldBe expect } } } @@ -24,167 +24,39 @@ class HandScoreTest : DescribeSpec({ val hand = hand(card(Rank.TEN), card(Rank.TEN), card(Rank.ACE)) it("Ace를 1점으로 계산한다") { - HandScore.from(hand).cardScore shouldBe 21 + HandScore.from(hand).value shouldBe 21 } } context("Ace가 없는 카드에서 합이 11이하라면") { val hand = hand(card(Rank.TWO), card(Rank.THREE)) - it("모든 점수를 그대로 계산") { - HandScore.from(hand).cardScore shouldBe 5 + it("모든 점수를 그대로 계산한다") { + HandScore.from(hand).value shouldBe 5 } } } - describe("gameScore") { - context("BUST 일 때") { - val hand = HandScore(22) - hand.isBust shouldBe true + describe("isGreaterOrEqualToMaxScore") { + context("21점 미만일 때") { + val handScore = HandScore(20) - it("0점 반환") { - hand.gameScore shouldBe 0 - } - } - - context("BUST가 아닐 때") { - val hand = HandScore(21) - hand.isBust shouldBe false - - it("cardScore 반환") { - hand.gameScore shouldBe 21 - } - } - } - - describe("isBust") { - context("21을 넘었을 때") { - val handScore = HandScore(22) - it("true 반환") { - handScore.isBust shouldBe true - } - } - - context("21을 넘지 않았을 때") { - val handScore = HandScore(21) it("false 반환") { - handScore.isBust shouldBe false - } - } - } - - describe("isGreaterCardScoreThan") { - val score = HandScore(16) - context("보다 작은 점수를 비교") { - val result = score isGreaterCardScoreThan 15 - it("참을 반환") { - result shouldBe true - } - } - context("보다 큰 점수를 비교") { - val result = score isGreaterCardScoreThan 20 - it("거짓을 반환") { - result shouldBe false - } - } - context("같은 점수를 비교") { - val result = score isGreaterCardScoreThan 16 - it("거짓을 반환") { - result shouldBe false - } - } - } - - describe("isGreaterGameScoreThan") { - context("보다 작은 점수를 비교") { - val score = HandScore(16) - val result = score isGreaterCardScoreThan 15 - it("참을 반환") { - result shouldBe true - } - } - - context("보다 큰 점수를 비교") { - val score = HandScore(16) - val result = score isGreaterCardScoreThan 20 - it("거짓을 반환") { - result shouldBe false - } - } - context("같은 점수를 비교") { - val score = HandScore(16) - val result = score isGreaterCardScoreThan 16 - it("거짓을 반환") { - result shouldBe false - } - } - - context("BUST VS other: 더 낮은 cardScore") { - val score = HandScore(22) - score.isBust shouldBe true - - val result = score isGreaterGameScoreThan HandScore(10) - - it("거짓 반환") { - result shouldBe false - } - } - - context("더 낮은 cardScore VS other: BUST") { - val score = HandScore(10) - score.isBust shouldBe false - - val other = HandScore(22) - other.isBust shouldBe true - - val result = score isGreaterGameScoreThan other - - it("참 반환") { - result shouldBe true - } - } - - context("BUST VS other: BUST") { - val score = HandScore(23) - score.isBust shouldBe true - - val other = HandScore(22) - other.isBust shouldBe true - - val result = score isGreaterGameScoreThan other - - it("거짓 반환") { - result shouldBe false - } - } - } - - describe("isSameGameScoreThan") { - context("같은 점수를 비교") { - val score = HandScore(16) - val result = score isSameGameScoreTo HandScore(16) - it("참을 반환") { - result shouldBe true + handScore.isGreaterOrEqualToMaxScore shouldBe false } } + context("21점일 때") { + val handScore = HandScore(21) - context("다른 점수를 비교") { - val score = HandScore(16) - val result = score isSameGameScoreTo HandScore(20) - it("거짓 반환") { - result shouldBe false + it("true 반환") { + handScore.isGreaterOrEqualToMaxScore shouldBe true } } + context("21점 초과일 때") { + val handScore = HandScore(22) - context("다른 cardScore이지만 둘 다 BUST일 때") { - val score = HandScore(22) - val other = HandScore(22) - score.isBust shouldBe true - other.isBust shouldBe true - - val result = score isSameGameScoreTo other - it("참 반환") { - result shouldBe true + it("true 반환") { + handScore.isGreaterOrEqualToMaxScore shouldBe true } } } diff --git a/src/test/kotlin/blackjack/domain/distirbution/DealEndTest.kt b/src/test/kotlin/blackjack/domain/distirbution/DealEndTest.kt index da38b63cb..549b27d00 100644 --- a/src/test/kotlin/blackjack/domain/distirbution/DealEndTest.kt +++ b/src/test/kotlin/blackjack/domain/distirbution/DealEndTest.kt @@ -11,7 +11,7 @@ import io.kotest.matchers.shouldBe class DealEndTest : DescribeSpec({ describe("deal") { - context("테이블의 플레이어와 딜러") { + context("DealEnd 의 카드 배분을 하면") { val players = players(player("currentPlayer", Action.HIT), player()) val dealer = Dealer() val table = table(dealer = dealer, players = players) @@ -19,25 +19,25 @@ class DealEndTest : DescribeSpec({ val result = dealEnd.deal() - it("결과의 플레이어는 테이블의 플레이어") { - result.playersResult.forEachIndexed { index, playerResult -> - playerResult.player shouldBe players.value[index] + it("결과에 반환된 플레이어들은 테이블의 모든 플레이어다") { + result.players.value.forEachIndexed { index, player -> + player shouldBe players.value[index] } } - it("결과의 딜러는 테이블의 딜러") { - result.dealerResults.dealer shouldBe dealer + it("결과의 반환된 딜러는 테이블의 딜러다") { + result.dealer shouldBe dealer } } } describe("nextDistributor") { - context("dealEnd의 다음 배분자 호출") { + context("DealEnd의 다음 배분자 호출하면") { val players = players(player("currentPlayer", Action.HIT), player()) val dealer = Dealer() val table = table(dealer = dealer, players = players) val dealEnd = DealEnd(table) - it("호출 실패") { + it("다음 배분자가 없어 호출에 실패한다") { shouldThrowExactly { dealEnd.nextDistributor() } diff --git a/src/test/kotlin/blackjack/domain/distirbution/DealInitialCardsTest.kt b/src/test/kotlin/blackjack/domain/distirbution/DealInitialCardsTest.kt index 73ca124dc..b080bae0b 100644 --- a/src/test/kotlin/blackjack/domain/distirbution/DealInitialCardsTest.kt +++ b/src/test/kotlin/blackjack/domain/distirbution/DealInitialCardsTest.kt @@ -7,22 +7,22 @@ import io.kotest.matchers.types.shouldBeTypeOf class DealInitialCardsTest : DescribeSpec({ describe("deal") { - context("첫 카드 배분 스테이지를 진행시키면") { + context("DealInitialCards 카드 배분을 하면") { val table = table() val dealInitialCards = DealInitialCards(table) dealInitialCards.deal() - it("플레이어마다 2장의 카드 수령") { + it("플레이어마다 2장의 카드를 수령한다") { table.players.value.forEach { player -> player.hand.cards.size shouldBe 2 } } - it("딜러도 2장의 카드 수령") { + it("딜러는 2장의 카드를 수령한다") { table.dealer.hand.cards.size shouldBe 2 } - it("다음 카드 배분은 플레이어에게 카드 배분") { + it("다음 카드 배분은 DealToPlayer이다") { dealInitialCards.nextDistributor.shouldBeTypeOf() } } diff --git a/src/test/kotlin/blackjack/domain/distirbution/DealToDealerTest.kt b/src/test/kotlin/blackjack/domain/distirbution/DealToDealerTest.kt index 2f7660b8b..e8c22ffa2 100644 --- a/src/test/kotlin/blackjack/domain/distirbution/DealToDealerTest.kt +++ b/src/test/kotlin/blackjack/domain/distirbution/DealToDealerTest.kt @@ -26,11 +26,11 @@ class DealToDealerTest : DescribeSpec({ table.dealer.hand.cards.size shouldBe 3 } - it("배분 결과 참을 반환") { + it("배분이 진행되어 배분 결과는 참을 반환한다") { result.isHit shouldBe true } - it("게임의 다음 배분은 종료 상태") { + it("게임의 다음 배분은 DealEnd이다") { dealToDealer.nextDistributor.shouldBeTypeOf() } } @@ -48,11 +48,11 @@ class DealToDealerTest : DescribeSpec({ table.dealer.hand.cards.size shouldBe 2 } - it("배분 결과 거짓을 반환") { + it("배분이 진행되지 않아 배분 결과는 거짓을 반환한다") { result.isHit shouldBe false } - it("게임의 다음 배분은 종료 상태") { + it("게임의 다음 배분은 DealEnd 이다") { dealToDealer.nextDistributor.shouldBeTypeOf() } } diff --git a/src/test/kotlin/blackjack/domain/distirbution/DealToPlayerTest.kt b/src/test/kotlin/blackjack/domain/distirbution/DealToPlayerTest.kt index cd92c70f4..68e488728 100644 --- a/src/test/kotlin/blackjack/domain/distirbution/DealToPlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/distirbution/DealToPlayerTest.kt @@ -14,7 +14,7 @@ import io.kotest.matchers.types.shouldBeTypeOf class DealToPlayerTest : DescribeSpec({ describe("deal") { - context("플레이어가 HIT (player: HIT, 점수 : bust 아님) 을 하면") { + context("플레이어가 HIT (player: HIT, 점수 : 21점 미만) 을 하면") { val players = players(player("currentPlayer", Action.HIT), player()) val table = table(inputAction = Action.HIT, players = players) val deckCount = table.dealer.deck.cards.size @@ -24,24 +24,24 @@ class DealToPlayerTest : DescribeSpec({ val result = dealToPlayer.deal() - it("플레이어의 카드는 증가") { + it("플레이어의 카드 1장이 증가한다") { table.players.inTurn.hand.cards.size shouldBe handCount + 1 } - it("덱에서 카드는 제거") { + it("덱에서 카드 1장은 제거된다") { table.dealer.deck.cards.size shouldBe deckCount - 1 } - it("결과 값의 플레이어는 이번 차례 진행한 플레이어") { + it("결과 값에 반환된 플레이어는 이번 차례 진행한 플레이어다") { result.player.name shouldBe PlayerName("currentPlayer") } - it("게임 룰에 의한 STAND인지 여부는 FALSE") { + it("플레이어가 HIT을 한 결과이므로 게임 룰에 의한 STAND인지 여부는 FALSE") { result.isSystemStand shouldBe false } } - context("모든 플레이어는 항상 STAND를 응답(player: STAND)할 때") { + context("플레이어가 STAND를 응답(player: STAND)했을 때") { val player1Cards = hand(card(Rank.TEN), card(Rank.TEN)) val players = players( player("currentPlayer", Action.STAND, player1Cards), @@ -49,72 +49,72 @@ class DealToPlayerTest : DescribeSpec({ ) val table = table(inputAction = Action.HIT, players = players) - context("첫번째 플레이어가 STAND을 하면") { + context("첫번째 플레이어의 STAND였다면") { val dealToPlayer = DealToPlayer(table) val result = dealToPlayer.deal() - it("게임의 다음 상태는 다음 플레이어 배분 차례") { + it("게임의 다음 배분은 DealToPlayer 다") { dealToPlayer.nextDistributor.shouldBeTypeOf() table.players.inTurn.name shouldBe PlayerName("nextPlayer") } - it("플레이어 카드는 변화 없음") { + it("플레이어 카드는 변화 없다") { table.players.value.first().hand shouldBe player1Cards } - it("결과 값의 플레이어는 이번 차례 진행한 플레이어") { + it("결과 값에 반환된 플레이어는 이번 차례 진행한 플레이어다") { result.player.name shouldBe PlayerName("currentPlayer") } - it("게임 룰에 의한 STAND인지 여부는 FALSE") { + it("플레이어가 STAND를 했으므로 게임 룰에 의한 STAND인지 여부는 FALSE이다") { result.isSystemStand shouldBe false } } - context("카드 배분시 두 번째 플레이어가 STAND을 한 경우") { + context("두 번째 플레이어의 STAND였다면") { table.players.inTurn.name shouldBe PlayerName("nextPlayer") val dealToPlayer = DealToPlayer(table) val result = dealToPlayer.deal() - it("딜러 카드 배분 차례") { + it("게임의 다음 배분은 DealToDealer 다") { dealToPlayer.nextDistributor.shouldBeTypeOf() } - it("결과 값의 플레이어는 이번 차례 진행한 플레이어") { + it("결과 값에 반환된 플레이어는 이번 차례 진행한 플레이어다") { result.player.name shouldBe PlayerName("nextPlayer") } - it("게임 룰에 의한 STAND인지 여부는 FALSE") { + it("플레이어가 STAND를 했으므로 게임 룰에 의한 STAND인지 여부는 FALSE다") { result.isSystemStand shouldBe false } } + } - context("카드 배분시 게임 룰에 의한 (21점 이상) STAND을 한 경우") { - val cards = hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TEN)) - val players = players(player("currentPlayer", hand = cards, action = Action.HIT), player("nextPlayer")) + context("첫번째 플레이어가 게임 룰에 의한 (21점 이상) STAND인 경우") { + val cards = hand(card(Rank.TEN), card(Rank.ACE)) + val players = players(player("currentPlayer", hand = cards, action = Action.HIT), player("nextPlayer")) - val table = table(inputAction = Action.HIT, players = players) - table.players.inTurn.name shouldBe PlayerName("currentPlayer") - val dealToPlayer = DealToPlayer(table) + val table = table(inputAction = Action.HIT, players = players) + table.players.inTurn.name shouldBe PlayerName("currentPlayer") + val dealToPlayer = DealToPlayer(table) - val result = dealToPlayer.deal() + val result = dealToPlayer.deal() - it("게임의 다음 상태는 다음 플레이어 배분 차례") { - dealToPlayer.nextDistributor.shouldBeTypeOf() - table.players.inTurn.name shouldBe PlayerName("nextPlayer") - } + it("게임의 다음 배분은 DealToPlayer 다") { + dealToPlayer.nextDistributor.shouldBeTypeOf() + table.players.inTurn.name shouldBe PlayerName("nextPlayer") + } - it("플레이어 카드는 변화 없음") { - table.players.value.first().hand shouldBe cards - } + it("플레이어 카드는 변화 없다") { + table.players.value.first().hand shouldBe cards + } - it("결과 값의 플레이어는 이번 차례 진행한 플레이어") { - result.player.name shouldBe PlayerName("currentPlayer") - } + it("결과 값에 반환된 플레이어는 이번 차례 진행한 플레이어다") { + result.player.name shouldBe PlayerName("currentPlayer") + } - it("게임 룰에 의한 STAND인지 여부는 TRUE") { - result.isSystemStand shouldBe true - } + it("게임 룰에 의한 STAND인지 여부는 TRUE다") { + result.isSystemStand shouldBe true } } } diff --git a/src/test/kotlin/blackjack/domain/player/DealerPlayerTest.kt b/src/test/kotlin/blackjack/domain/player/DealerPlayerTest.kt index 024036001..5a5dc5263 100644 --- a/src/test/kotlin/blackjack/domain/player/DealerPlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/player/DealerPlayerTest.kt @@ -9,7 +9,6 @@ import io.kotest.matchers.shouldBe class DealerPlayerTest : DescribeSpec({ describe("hitOrStand") { - context("16점 이하라면") { val score16Cards = hand( card(Rank.EIGHT), @@ -17,7 +16,7 @@ class DealerPlayerTest : DescribeSpec({ ) val player = DealerPlayer(score16Cards) - it("HIT 반환") { + it("HIT가 반환된다") { val result = player.hitOrStand() result shouldBe Action.HIT @@ -31,7 +30,7 @@ class DealerPlayerTest : DescribeSpec({ ) val player = DealerPlayer(score20Cards) - it("STAND 반환") { + it("STAND가 반환된다") { val result = player.hitOrStand() result shouldBe Action.STAND diff --git a/src/test/kotlin/blackjack/domain/player/PlayerNamesTest.kt b/src/test/kotlin/blackjack/domain/player/PlayerNamesTest.kt index b3f3e42d9..169cfc65a 100644 --- a/src/test/kotlin/blackjack/domain/player/PlayerNamesTest.kt +++ b/src/test/kotlin/blackjack/domain/player/PlayerNamesTest.kt @@ -1,19 +1,30 @@ package blackjack.domain.player +import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe class PlayerNamesTest : DescribeSpec({ - describe("플레이어 이름 목록 생성") { + describe("PlayerNames.from()") { context("플레이어 이름이 문자열 리스트로 주어지면") { val names = listOf("홍길동", "백상어") val result = PlayerNames.from(names) - it("주어진 문자열로 이름 목록이 생성") { + it("주어진 문자열로 이름 목록이 생성된다") { result.value[0].value shouldBe "홍길동" result.value[1].value shouldBe "백상어" } } + + context("두 플레이어 이름이 동일하면") { + val names = listOf("홍길동", "홍길동") + + it("플레이어 이름 생성에 실패한다") { + shouldThrowExactly { + PlayerNames.from(names) + } + } + } } }) diff --git a/src/test/kotlin/blackjack/domain/player/PlayerTest.kt b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt index 5d04b0400..2c8fcbb06 100644 --- a/src/test/kotlin/blackjack/domain/player/PlayerTest.kt +++ b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt @@ -11,13 +11,13 @@ import blackjack.mock.player import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe -import java.lang.IllegalArgumentException +import io.kotest.matchers.shouldNotBe class PlayerTest : DescribeSpec({ describe("Player()") { context("플레이어 이름이 주어지면") { val name = PlayerName("홍길동") - it("플레이어 생성") { + it("플레이어가 생성된다") { val result = Player(name, { Action.HIT }) result.name shouldBe name @@ -32,7 +32,7 @@ class PlayerTest : DescribeSpec({ player.addCard(card) - it("플레이가 소유한 카드에 카드가 추가") { + it("플레이가 소유한 카드에 해당 카드가 추가된다") { player.hand.cards shouldBe listOf(card) } } @@ -44,7 +44,7 @@ class PlayerTest : DescribeSpec({ player.addCard(newCard) - it("플레이가 소유한 카드에 카드가 추가") { + it("플레이가 소유한 카드에 해당 카드가 추가") { player.hand shouldBe Hand(mutableListOf(oldCard, newCard)) } } @@ -59,47 +59,66 @@ class PlayerTest : DescribeSpec({ } } } + + context("플레이어가 21점이라면") { + val score21Cards = hand(card(Rank.TEN), card(Rank.ACE)) + val player = player(hand = score21Cards) + + it("카드를 추가할 수 없다") { + shouldThrowExactly { + player.addCard(Card(Suit.CLUB, Rank.TEN)) + } + } + } } - describe("isBust") { + describe("isGreaterOrEqualToMaxScore") { context("21을 넘었을 때") { val player = player(hand = hand(card(Rank.THREE), card(Rank.TEN), card(Rank.TEN))) - it("true 반환") { - player.isBust shouldBe true + it("true가 반환된다") { + player.isGreaterOrEqualToMaxScore shouldBe true + } + } + + context("21점일 때") { + val player = player(hand = hand(card(Rank.ACE), card(Rank.TEN))) + it("true가 반환된다") { + player.isGreaterOrEqualToMaxScore shouldBe true } } context("21을 넘지 않았을 때") { val player = player(hand = hand(card(Rank.ACE), card(Rank.ACE), card(Rank.ACE))) - it("false 반환") { - player.isBust shouldBe false + it("false가 반환된다") { + player.isGreaterOrEqualToMaxScore shouldBe false } } } describe("score") { - val player = player(hand = hand(card(Rank.ACE), card(Rank.TEN))) + context("플레이어가 21점의 카드를 갖고 있었다면") { + val player = player(hand = hand(card(Rank.ACE), card(Rank.TEN))) - context("플레이어의 점수 조회") { val result = player.score - it("플레이어 점수 반환") { - result.cardScore shouldBe 21 + + it("플레이어 점수는 21점이다") { + result.value shouldBe 21 } } } describe("hitOrStand") { context("이미 점수가 최대 점수 21을 넘었다면") { - val score30Cards = hand(card(Rank.QUEEN), card(Rank.QUEEN), card(Rank.QUEEN),) - it("HIT을 받아도 STAND가 반환") { + val score30Cards = hand(card(Rank.QUEEN), card(Rank.QUEEN), card(Rank.QUEEN)) + it("HIT을 받아도 STAND가 반환된다") { val player = player(action = Action.HIT, hand = score30Cards) val result = player.hitOrStand() result shouldBe Action.STAND } - it("STAND를 받으면 STAND가 반환") { + it("STAND를 받으면 STAND가 반환된다") { val player = player(action = Action.STAND, hand = score30Cards) val result = player.hitOrStand() @@ -108,17 +127,35 @@ class PlayerTest : DescribeSpec({ } } + context("이미 점수가 최대 점수 21점 이라면") { + val score21Cards = hand(card(Rank.QUEEN), card(Rank.ACE)) + it("HIT을 받아도 STAND가 반환된다") { + val player = player(action = Action.HIT, hand = score21Cards) + + val result = player.hitOrStand() + + result shouldBe Action.STAND + } + it("STAND를 받으면 STAND가 반환된다") { + val player = player(action = Action.STAND, hand = score21Cards) + + val result = player.hitOrStand() + + result shouldBe Action.STAND + } + } + context("최대 점수 21을 넘지 않았을 때") { val score5Cards = hand(card(Rank.TWO), card(Rank.THREE)) - it("HIT을 받으면 HIT이 반한") { + it("HIT을 받으면 HIT이 반한된다") { val player = player(action = Action.HIT, hand = score5Cards) val result = player.hitOrStand() result shouldBe Action.HIT } - it("STAND를 받으면 STAND가 반환") { + it("STAND를 받으면 STAND가 반환된다") { val player = player(action = Action.STAND, hand = score5Cards) val result = player.hitOrStand() @@ -127,4 +164,29 @@ class PlayerTest : DescribeSpec({ } } } + + describe("equals") { + context("두 플레이어의 이름이 다르면") { + val action = { _: Player -> Action.HIT } + val hand = hand() + + val player1 = Player(PlayerName("kim"), action, hand) + val player2 = Player(PlayerName("lee"), action, hand) + + it("다른 플레이어로 취급한다") { + player1 shouldNotBe player2 + } + } + } + + context("두 플레이어의 이름이 같다면") { + val action = { _: Player -> Action.HIT } + + val player1 = Player(PlayerName("kim"), action, hand(card(Rank.ACE))) + val player2 = Player(PlayerName("kim"), action, hand(card(Rank.TEN))) + + it("같은 플레이어로 취급한다") { + player1 shouldBe player2 + } + } }) diff --git a/src/test/kotlin/blackjack/domain/player/PlayersTest.kt b/src/test/kotlin/blackjack/domain/player/PlayersTest.kt index 3b658779f..d68806822 100644 --- a/src/test/kotlin/blackjack/domain/player/PlayersTest.kt +++ b/src/test/kotlin/blackjack/domain/player/PlayersTest.kt @@ -1,11 +1,7 @@ package blackjack.domain.player import blackjack.domain.Action -import blackjack.domain.card.Rank -import blackjack.mock.card -import blackjack.mock.hand import blackjack.mock.player -import blackjack.mock.players import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -18,18 +14,18 @@ class PlayersTest : DescribeSpec({ val names = PlayerNames(listOf(name1, name2)) val result = Players.of(names) { Action.HIT } - it("주어진 이름 순서대로 플레이어들 생성") { + it("주어진 이름 순서대로 플레이어들이 생성된다") { result.value[0].name shouldBe name1 result.value[1].name shouldBe name2 } - it("첫 이름의 플레이어가 첫 순번") { + it("첫 이름의 플레이어가 첫 순번이다") { result.inTurn.name shouldBe name1 } } context("플레이어가 2명이 아닌 경우") { val playerNames = PlayerNames(listOf(PlayerName("홍길동"), PlayerName("베트맨"), PlayerName("아이언맨"))) - it("플레이어 생성 실패") { + it("플레이어 생성에 실패한다") { shouldThrowExactly { Players.of(playerNames) { Action.HIT } } @@ -37,32 +33,14 @@ class PlayersTest : DescribeSpec({ } } - describe("현재 플레이어가 최대 점수를 넘었는지 여부 반환") { - context("현재 플레이어가 최대 점수를 넘었을 경우") { - val playerOverMaxScore = player(hand = hand(card(Rank.THREE), card(Rank.TEN), card(Rank.TEN))) - val players = players(playerOverMaxScore, player()) - it("true 반환") { - players.isPlayerInTurnOverMaxScore shouldBe true - } - } - - context("현재 플레이어가 최대 점수를 넘지 않았을 경우") { - val playerUnderMaxScore = player(hand = hand(card(Rank.ACE), card(Rank.TEN))) - val players = players(playerUnderMaxScore, player()) - it("false 반환") { - players.isPlayerInTurnOverMaxScore shouldBe false - } - } - } - - describe("다음 플레이어에게 차례 넘김") { + describe("changePlayer()") { val playerList = listOf(player("kim"), player("lee")) val players = Players(playerList) context("플레이어 1이 차례인 경우") { players.inTurn shouldBe playerList.first() players.changePlayer() - it("플레이어 2에게 차례가 넘어감") { + it("플레이어 2에게 차례가 넘어간다") { players.inTurn shouldBe playerList.last() } } @@ -70,7 +48,7 @@ class PlayersTest : DescribeSpec({ context("플레이어 2가 차례인 경우") { players.inTurn shouldBe playerList.last() - it("턴이 끝났다는 에러") { + it("턴이 끝났다는 에러가 발생한다") { shouldThrowExactly { players.changePlayer() } @@ -78,13 +56,13 @@ class PlayersTest : DescribeSpec({ } } - describe("마지막 플레이어 차례인지 조회") { + describe("isLastTurn") { val playerList = listOf(player("kim"), player("lee")) val players = Players(playerList) context("플레이어 1이 차례인 경우") { players.inTurn shouldBe playerList.first() - it("false 반환") { + it("false가 반환된다") { players.isLastTurn shouldBe false } } @@ -93,7 +71,7 @@ class PlayersTest : DescribeSpec({ players.changePlayer() players.inTurn shouldBe playerList.last() - it("true 반환") { + it("true가 반환된다") { players.isLastTurn shouldBe true } } diff --git a/src/test/kotlin/blackjack/domain/result/game/VictoryStatusTest.kt b/src/test/kotlin/blackjack/domain/result/distribution/VictoryStatusTest.kt similarity index 89% rename from src/test/kotlin/blackjack/domain/result/game/VictoryStatusTest.kt rename to src/test/kotlin/blackjack/domain/result/distribution/VictoryStatusTest.kt index f469a4778..de72aca58 100644 --- a/src/test/kotlin/blackjack/domain/result/game/VictoryStatusTest.kt +++ b/src/test/kotlin/blackjack/domain/result/distribution/VictoryStatusTest.kt @@ -1,5 +1,6 @@ -package blackjack.domain.result.game +package blackjack.domain.result.distribution +import blackjack.domain.result.game.VictoryStatus import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe diff --git a/src/test/kotlin/blackjack/domain/result/game/GameResultTest.kt b/src/test/kotlin/blackjack/domain/result/game/GameResultTest.kt deleted file mode 100644 index de5fe1c56..000000000 --- a/src/test/kotlin/blackjack/domain/result/game/GameResultTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -package blackjack.domain.result.game - -import blackjack.domain.Dealer -import blackjack.domain.card.Rank -import blackjack.domain.player.DealerPlayer -import blackjack.domain.player.Players -import blackjack.mock.card -import blackjack.mock.hand -import blackjack.mock.player -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.shouldBe - -class GameResultTest : DescribeSpec({ - describe("GameResult of (dealer, players)") { - val score25Cards = - hand(card(Rank.TEN), card(Rank.TEN), card(Rank.FIVE)) - val score22Cards = - hand(card(Rank.TEN), card(Rank.TEN), card(Rank.TWO)) - val score21Cards = - hand(card(Rank.TEN), card(Rank.ACE)) - val score10Cards = hand(card(Rank.TEN)) - val score5Cards = hand(card(Rank.FIVE)) - - context("dealer(21) > player1(10) > player2(5)") { - val dealer = Dealer(player = DealerPlayer(score21Cards)) - val player1 = player(hand = score10Cards) - val player2 = player(hand = score5Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : LOSS") { - result.playersResult.first().status shouldBe VictoryStatus.LOSS - } - it("player2 : LOSS") { - result.playersResult.last().status shouldBe VictoryStatus.LOSS - } - it("dealer : (WIN, WIN)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.WIN, VictoryStatus.WIN) - } - } - - context("player1(21) > player2(10) > dealer(5)") { - val dealer = Dealer(player = DealerPlayer(score5Cards)) - val player1 = player(hand = score21Cards) - val player2 = player(hand = score10Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : WIN") { - result.playersResult.first().status shouldBe VictoryStatus.WIN - } - - it("player2 : WIN") { - result.playersResult.last().status shouldBe VictoryStatus.WIN - } - - it("dealer : (LOSS, LOSS)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.LOSS, VictoryStatus.LOSS) - } - } - - context("player1(21) > dealer(10) > player2(5)") { - val dealer = Dealer(player = DealerPlayer(score10Cards)) - val player1 = player(hand = score21Cards) - val player2 = player(hand = score5Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : WIN") { - result.playersResult.first().status shouldBe VictoryStatus.WIN - } - - it("player2 : LOSS") { - result.playersResult.last().status shouldBe VictoryStatus.LOSS - } - - it("dealer : (LOSS, WIN)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.LOSS, VictoryStatus.WIN) - } - } - - context("dealer(5) > player1(22, BUST) == player2(22, BUST)") { - val dealer = Dealer(player = DealerPlayer(score5Cards)) - val player1 = player(hand = score22Cards) - val player2 = player(hand = score22Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : LOSS") { - result.playersResult.first().status shouldBe VictoryStatus.LOSS - } - - it("player2 : LOSS") { - result.playersResult.last().status shouldBe VictoryStatus.LOSS - } - - it("dealer : (WIN, WIN)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.WIN, VictoryStatus.WIN) - } - } - - context("player1(5) == player2(5) > dealer(22, BUST)") { - val dealer = Dealer(player = DealerPlayer(score22Cards)) - val player1 = player(hand = score5Cards) - val player2 = player(hand = score5Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : WIN") { - result.playersResult.first().status shouldBe VictoryStatus.WIN - } - - it("player2 : WIN") { - result.playersResult.last().status shouldBe VictoryStatus.WIN - } - - it("dealer : (LOSS, LOSS)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.LOSS, VictoryStatus.LOSS) - } - } - - context("player1(22, BUST) == player2(25, BUST) == dealer(22, BUST)") { - val dealer = Dealer(player = DealerPlayer(score22Cards)) - val player1 = player(hand = score22Cards) - val player2 = player(hand = score25Cards) - - val result = GameResult.of(Players(listOf(player1, player2)), dealer) - - it("player1 : LOSS") { - result.playersResult.first().status shouldBe VictoryStatus.LOSS - } - - it("player2 : LOSS") { - result.playersResult.last().status shouldBe VictoryStatus.LOSS - } - - it("dealer : (WIN, WIN)") { - result.dealerResults.status.value shouldBe listOf(VictoryStatus.WIN, VictoryStatus.WIN) - } - } - } -}) diff --git a/src/test/kotlin/blackjack/domain/result/game/ProfitTest.kt b/src/test/kotlin/blackjack/domain/result/game/ProfitTest.kt new file mode 100644 index 000000000..af67f3281 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/result/game/ProfitTest.kt @@ -0,0 +1,22 @@ +package blackjack.domain.result.game + +import blackjack.mock.profit +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class ProfitTest : StringSpec({ + "profit의 반대 (-profit) 금액을 구할 수 있다" { + val profit = profit(1_000) + + profit.negative shouldBe profit(-1_000) + } + + "profit끼리 덧셈이 가능하다" { + val profit1 = profit(1_000) + val profit2 = profit(1_000) + + val result = profit1 + profit2 + + result shouldBe profit(2_000) + } +}) diff --git a/src/test/kotlin/blackjack/mock/InputProcessorMock.kt b/src/test/kotlin/blackjack/mock/InputProcessorMock.kt index 9a36366a2..77cc29cc8 100644 --- a/src/test/kotlin/blackjack/mock/InputProcessorMock.kt +++ b/src/test/kotlin/blackjack/mock/InputProcessorMock.kt @@ -2,15 +2,18 @@ package blackjack.mock import blackjack.controller.InputProcessor import blackjack.domain.Action +import blackjack.domain.batting.BetAmount import blackjack.domain.player.Player import blackjack.domain.player.PlayerNames class InputProcessorMock( private val playerNames: List = listOf("kim", "lee"), private val action: Action = Action.HIT, + private val betAmount: BetAmount = BetAmount(amount(3_000)) ) : InputProcessor { - override fun playerNames(): PlayerNames = playerNames.let(PlayerNames::from) + override fun playerNames(): PlayerNames = PlayerNames.from(playerNames) + override fun playerBet(player: Player): BetAmount = betAmount override fun playerAction(player: Player): Action = action } diff --git a/src/test/kotlin/blackjack/mock/TestFactories.kt b/src/test/kotlin/blackjack/mock/TestFactories.kt index 350ef0de5..d5ce58a01 100644 --- a/src/test/kotlin/blackjack/mock/TestFactories.kt +++ b/src/test/kotlin/blackjack/mock/TestFactories.kt @@ -3,6 +3,7 @@ package blackjack.mock import blackjack.domain.Action import blackjack.domain.Dealer import blackjack.domain.GameTable +import blackjack.domain.batting.Amount import blackjack.domain.card.Card import blackjack.domain.card.Deck import blackjack.domain.card.Hand @@ -11,14 +12,15 @@ import blackjack.domain.card.Suit import blackjack.domain.player.Player import blackjack.domain.player.PlayerName import blackjack.domain.player.Players +import blackjack.domain.result.game.Profit -fun card(rank: Rank, suit: Suit = Suit.CLUB): Card = Card(suit, rank) +fun card(rank: Rank = Rank.TEN, suit: Suit = Suit.CLUB): Card = Card(suit, rank) fun hand(vararg cards: Card): Hand = Hand(cards.toMutableList()) -fun deck(vararg cards: Card): Deck = Deck(cards.toMutableList().let(::ArrayDeque)) +fun deck(vararg cards: Card): Deck = Deck(ArrayDeque(cards.toMutableList())) -fun deck(cards: List): Deck = Deck(cards.let(::ArrayDeque)) +fun deck(cards: List): Deck = Deck(ArrayDeque(cards)) fun player( name: String = "kim", @@ -37,6 +39,9 @@ fun table( players: Players? = null, ): GameTable = GameTable( + players ?: players(player("kim", inputAction), player("lee", inputAction)), dealer, - players ?: players(player("kim", inputAction), player("lee", inputAction)) ) + +fun amount(amount: Int): Amount = Amount(amount.toBigDecimal()) +fun profit(profit: Int): Profit = Profit(profit.toBigDecimal())