Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Step4] 4단계 - 블랙잭(베팅) #697

Open
wants to merge 19 commits into
base: bong6981
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ed22ad5
[step4] docs: 기능 목록 정리
bong6981 Dec 2, 2023
ea2d901
[step4] feat: 21점이면 더이상 카드를 받지 못하도록 변경
bong6981 Dec 2, 2023
d095226
[step3] chore: getDesiredAction -> desiredAction 네이밍 변경
bong6981 Dec 2, 2023
b1ebc8a
[step4] feat: 플레이어 이름을 식별자로 사용
bong6981 Dec 2, 2023
158af44
[step4] feat: 플레이어별 베팅 금액 저장
bong6981 Dec 2, 2023
c8eca31
[step4] feat: 베팅 금액에 따른 수익 계산 및 출력
bong6981 Dec 3, 2023
3d30c05
[step3] refactor: View를 보여줄지 말지는 ViewResultProcessor에서 처리
bong6981 Dec 3, 2023
5181cef
[step3] refactor: requireNotNul()을 활용
bong6981 Dec 3, 2023
fc1d9ae
[step3] refactor: step3 피드백 반영
bong6981 Dec 3, 2023
a4db3db
[step4] chore: 생성 로직을 앞단으로 이동
bong6981 Dec 6, 2023
780067a
[step4] feat: PlayerGameResult 의 player 접근제어자 -> private
bong6981 Dec 6, 2023
d95162a
[step4] test: DCI 패턴을 고려한 테스트 이름 수정
bong6981 Dec 6, 2023
00f52f0
[step4] feat: 게임 승패는 BlackJackJudge에서 판단하도록 변경
bong6981 Dec 6, 2023
8bbefe0
[step4] feat: 블랙잭 여부는 BlackJackJudge에서 판단하도록 변경
bong6981 Dec 6, 2023
f96543d
[step4] chore: BUST_THRESHOLD 대신 BLACK_JACK_SCORE 사사용
bong6981 Dec 6, 2023
ebae7ee
[step4] feat: BetAmount 분리
bong6981 Dec 31, 2023
c66f8bf
[step4] chore: 가독성을 위한 코드 줄바꿈 변경
bong6981 Dec 31, 2023
02e8ce0
[step4] chore: 불필요한 let 사용 삭제
bong6981 Dec 31, 2023
25c8735
[step4] refactor: 의미 없는 BUST_SCORE 사용 삭제
bong6981 Dec 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 80 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,87 @@
- git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

### 기능 목록
Copy link
Member

Choose a reason for hiding this comment

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

문서화 👍

게임 입력
- [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으로 계산한다.
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/controller/InputProcessor.kt
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 5 additions & 6 deletions src/main/kotlin/blackjack/controller/ResultProcessor.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
10 changes: 8 additions & 2 deletions src/main/kotlin/blackjack/controller/ViewInputProcessor.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
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
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
Expand Down
36 changes: 24 additions & 12 deletions src/main/kotlin/blackjack/controller/ViewResultProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
}
}
24 changes: 14 additions & 10 deletions src/main/kotlin/blackjack/domain/BlackJackGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackJudge.kt
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

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

21이라는 비즈니스 상수값이 BlackJackJudgeHandScore에 존재하고 있어요. 21을 이렇게 분산되어서 관리하면 하나의 클래스가 변경하여야할 때 다른쪽 클래스도 변경하는 것에 대해서 의식하지 못할 수도 있을 것 같은데 한군데서 관리할 수 있도록 하는 방법은 없을까요?

Copy link
Author

@bong6981 bong6981 Dec 31, 2023

Choose a reason for hiding this comment

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

제가 도메인에 대한 지식이 부족해서 발생한 코드 같습니다

  • 21점 이상을 가질 수 없다 / 블랙잭이라면 점수 21점을 얻는다. 이렇게 아예 다르게 인식했고, 그에 따라 각기 다른 클래스에 두게 되었습니다
  • BlackJackJudge 가 HansScore 의 MAX_SCORE 을 참조할 수도 있겠지만 이는 다른 성격이라 생각이 드는데용
    어떻게 생각하시는지 궁금합니다!


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
}
2 changes: 1 addition & 1 deletion src/main/kotlin/blackjack/domain/GameTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/blackjack/domain/batting/Amount.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/blackjack/domain/batting/BetAmount.kt
Original file line number Diff line number Diff line change
@@ -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))
}
Loading