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] 블랙잭(베팅) 구현 #816

Open
wants to merge 5 commits into
base: seokho-ham
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions 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.

from: #809 (comment)

저는 사실 테스트코드만을 위한 코드 변경은 해본적이 없어서 그런지 굳이 필요할까라고 생각이 들기는 합니다!
혹시 수현님은 이렇게 변경했을때 경험하신 이점들이 따로 있으셨을까요?

예를 들어 Cards의 add 함수가 바뀐다거나, Cards의 테스트 과정에서 많은 카드 더미가 있어야 하는 전제 조건이 있는 경우 이점을 얻을 수 있을 것 같아요.

저도 테스트 코드만을 위한 코드 변경은 최대한 보수적으로 접근하려고 하는데요, (예를 들어 테스트코드만을 위해 private -> public으로 전환한다던지?) 예외적으로 생성자 파라미터의 경우 객체를 초기화하는 전제 조건 주입 목적으로는 허용하고 있습니다! 외부에서 변경 가능하지 않도록 설계할 수 있기도 하구요!

이 부분은 재성님이 수업에서도 언급하셔서 제안드렸으나 개발자마다 주관적인 의견이 있을 것이라고 생각합니다 🙂

Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@
- [x] 딜러는 17 이상일 경우 카드를 받을 수 없다.
- [x] 딜러의 패가 21이 넘어가면 유저들의 패와 상관없이 유저들이 승리한다.
- [x] 결과를 출력한다.

## step4 요구사항

- [ ] 플레이어는 게임 시작 시 베팅 금액을 입력한다.
- [ ] 플레이어는 bust가 되면 베팅 금액을 모두 잃게 된다.
- [ ] 딜러가 bust가 되면 플레이어들은 베팅 금액을 받는다.
- [ ] 블랙잭일 경우 베팅 금액의 1.5배를 받는다.
- [ ] 딜러와 플레이어 모두 블랙잭일 경우 베팅한 금액만 돌려받는다.
19 changes: 14 additions & 5 deletions src/main/kotlin/blackjack/BlackJackGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import blackjack.domain.Deck
import blackjack.domain.Player
import blackjack.domain.PlayerGameResult
import blackjack.domain.PlayerStatus
import blackjack.domain.PlayerWinLoseResult
import blackjack.domain.Players
import blackjack.view.PlayerInfo

class BlackJackGame private constructor(
Copy link
Member

Choose a reason for hiding this comment

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

이전 단계에서 딜러와 플레이어의 중복 코드에 대한 고려 + 차이가 나는 로직에 대해 적절히 분리하는 작업이 선행되어서 이번 단계에서는 큰 변경사항이 없었네요!

이 부분이 사실 블랙잭 미션의 핵심 중 하나인데요, 딜러와 플레이어는 언뜻 보면 같은 참가자로 묶였을 때 더 직관적인 것처럼 보이나, 사실 분리했을 때 오히려 구조가 더 깔끔해집니다.

4단계를 진행하면서 이 구조의 이점이 잘 와닿으셨으면 좋겠네요!

Copy link
Member

Choose a reason for hiding this comment

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

혹시 어제의 수업을 듣고 상태 패턴을 적용해볼 계획도 있으신가요?
마지막 단계인 만큼 편하게 도전해보세요!

https://seonghoonc.tistory.com/14

Copy link
Author

Choose a reason for hiding this comment

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

마지막이니까 한번 도전해보고 다시 리뷰 요청 드리겠습니다:)

val dealer: Dealer,
Expand Down Expand Up @@ -40,7 +42,14 @@ class BlackJackGame private constructor(
}

fun getGameResult(): GameResult {
val playerGameResults = players.members.map { PlayerGameResult(it.name, it.compareWithDealer(dealer)) }
val playerGameResults =
players.members.map {
PlayerGameResult(
it.name,
it.betAmount,
PlayerWinLoseResult.compareResult(dealer, it),
)
}
val dealerResult = DealerResult(playerGameResults)
return GameResult(dealerResult, playerGameResults)
}
Expand All @@ -62,19 +71,19 @@ class BlackJackGame private constructor(
private const val DEFAULT_CARD_COUNT = 2

fun createGame(
playerNames: List<String>,
playerInfos: List<PlayerInfo>,
deck: Deck,
): BlackJackGame {
val dealer = createDealer(deck)
val players = createPlayers(playerNames, deck)
val players = createPlayers(playerInfos, deck)
return BlackJackGame(dealer, players, deck)
}

private fun createPlayers(
playerNames: List<String>,
playerInfos: List<PlayerInfo>,
deck: Deck,
): Players {
val players = Players.from(playerNames)
val players = Players.from(playerInfos)
players.drawDefaultCards(deck, DEFAULT_CARD_COUNT)
return players
}
Expand Down
24 changes: 8 additions & 16 deletions src/main/kotlin/blackjack/DealerResult.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
package blackjack

import blackjack.domain.CalculateEarnAmount
import blackjack.domain.PlayerGameResult
import blackjack.domain.PlayerWinLoseResult

class DealerResult(playerGameResults: List<PlayerGameResult>) {
var winCount: Int = 0
private set
var loseCount: Int = 0
private set
var pushCount: Int = 0
private set
class DealerResult(playerGameResults: List<PlayerGameResult>) : CalculateEarnAmount {
private var earnAmount: Int =
playerGameResults
.map { it.getEarnAmount() }
.fold(0) { total, amount -> total + amount }.toInt()

init {
playerGameResults.forEach {
when (it.result) {
PlayerWinLoseResult.WIN -> loseCount++
PlayerWinLoseResult.LOSE -> winCount++
PlayerWinLoseResult.PUSH -> pushCount++
}
}
override fun getEarnAmount(): Int {
return -earnAmount
Comment on lines +6 to +13
Copy link
Member

Choose a reason for hiding this comment

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

earnAmount가 variable이 될 필요가 있을까요~?

}
}
7 changes: 3 additions & 4 deletions src/main/kotlin/blackjack/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import blackjack.view.PlayerCommands
fun main() {
// 1. 유저명 입력 및 유저 생성
val deck = RandomDeck()
val userNames = InputView.getUserNames()
val game = BlackJackGame.createGame(userNames, deck)

val playerInfos = InputView.getPlayerInfos()
val game = BlackJackGame.createGame(playerInfos, deck)
OutputView.printCurrentStatus(game)

// 2-1. 블랙잭 여부 확인
Expand All @@ -36,7 +35,7 @@ private fun playBlackJack(game: BlackJackGame) {

private fun handlePlayerCommand(game: BlackJackGame) {
val player = game.currentPlayer
val command = InputView.getUserCommand(player)
val command = InputView.getPlayerCommand(player)
when (command) {
PlayerCommands.HIT -> game.hit(player)
PlayerCommands.STAY -> game.stay(player)
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/domain/BetAmount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack.domain

data class BetAmount(val amount: Double) {
init {
require(amount > ZERO) { "베팅 금액은 0원 이상이어야 합니다." }
}

companion object {
private const val ZERO = 0
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/blackjack/domain/CalculateEarnAmount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package blackjack.domain

fun interface CalculateEarnAmount {
fun getEarnAmount(): Int
Copy link
Member

Choose a reason for hiding this comment

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

어떤 의도로 만드신 인터페이스인지 이해했습니다!

다만 현재는 인터페이스의 Calculate~ 워딩이 함수처럼 느껴질 수도 있을 것 같아서 더 적절한 이름으로 바꿔볼 수 있을 것 같아요!

}
13 changes: 1 addition & 12 deletions src/main/kotlin/blackjack/domain/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package blackjack.domain

class Player(
val name: String,
val betAmount: BetAmount,
val hand: PlayerCards = PlayerCards(),
) {
var status: PlayerStatus = PlayerStatus.HIT
Expand Down Expand Up @@ -29,18 +30,6 @@ class Player(
return status == PlayerStatus.BLACKJACK
}

fun compareWithDealer(dealer: Dealer): PlayerWinLoseResult {
return when {
dealer.isBlackJack() -> PlayerWinLoseResult.LOSE
dealer.isBust() -> PlayerWinLoseResult.WIN
isBust() -> PlayerWinLoseResult.LOSE
isBlackJack() && !dealer.isBlackJack() -> PlayerWinLoseResult.WIN
dealer.getCardSum() > this.hand.calculateCardsMaxSum() -> PlayerWinLoseResult.LOSE
dealer.getCardSum() == this.hand.calculateCardsMaxSum() -> PlayerWinLoseResult.PUSH
else -> PlayerWinLoseResult.WIN
}
}

private fun validateName(name: String) {
require(name.isNotBlank()) { "유저의 이름은 공백일 수 없습니다." }
}
Expand Down
9 changes: 8 additions & 1 deletion src/main/kotlin/blackjack/domain/PlayerGameResult.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package blackjack.domain

import kotlin.math.roundToInt

data class PlayerGameResult(
val name: String,
val betAmount: BetAmount,
val result: PlayerWinLoseResult,
)
) : CalculateEarnAmount {
override fun getEarnAmount(): Int {
return (betAmount.amount * result.odds).roundToInt()
}
}
26 changes: 22 additions & 4 deletions src/main/kotlin/blackjack/domain/PlayerWinLoseResult.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
package blackjack.domain

enum class PlayerWinLoseResult {
WIN,
LOSE,
PUSH,
enum class PlayerWinLoseResult(val odds: Double) {
WIN(1.0),
LOSE(-1.0),
PUSH(0.0),
BLACKJACK(1.5), ;

companion object {
fun compareResult(
dealer: Dealer,
player: Player,
): PlayerWinLoseResult {
return when {
dealer.isBlackJack() -> LOSE
dealer.isBust() -> WIN
player.isBust() -> LOSE
player.isBlackJack() && !dealer.isBlackJack() -> BLACKJACK
dealer.getCardSum() > player.hand.calculateCardsMaxSum() -> LOSE
dealer.getCardSum() == player.hand.calculateCardsMaxSum() -> PUSH
else -> WIN
}
}
}
}
6 changes: 4 additions & 2 deletions src/main/kotlin/blackjack/domain/Players.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package blackjack.domain

import blackjack.view.PlayerInfo

class Players private constructor(
val members: List<Player>,
) {
Expand All @@ -25,8 +27,8 @@ class Players private constructor(
}

companion object {
fun from(names: List<String>): Players {
return Players(names.map { Player(it) })
fun from(names: List<PlayerInfo>): Players {
return Players(names.map { Player(it.name, BetAmount(it.amount)) })
}
}
}
14 changes: 12 additions & 2 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ import blackjack.domain.Player
object InputView {
private const val SEPARATOR = ","

fun getUserCommand(player: Player): PlayerCommands {
fun getPlayerCommand(player: Player): PlayerCommands {
println("${player.name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)")
return PlayerCommands.findByCommand(readln())
}

fun getUserNames(): List<String> {
fun getPlayerInfos(): List<PlayerInfo> {
val playerNames = getPlayerNames()
return playerNames.map { PlayerInfo(it, getBetAmountInput(it)) }
}

private fun getPlayerNames(): List<String> {
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)")
return readln().split(SEPARATOR)
}

private fun getBetAmountInput(playerName: String): Double {
println("${playerName}의 배팅 금액은?")
return readln().toDouble()
}
}
13 changes: 2 additions & 11 deletions src/main/kotlin/blackjack/view/OutputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import blackjack.domain.CardMark
import blackjack.domain.Cards
import blackjack.domain.Dealer
import blackjack.domain.Player
import blackjack.domain.PlayerWinLoseResult
import blackjack.domain.Players

object OutputView {
Expand Down Expand Up @@ -47,16 +46,8 @@ object OutputView {
val playerGameResults = gameResult.playerGameResults

println()
println("딜러: ${dealerResult.winCount}승 ${dealerResult.pushCount}무 ${dealerResult.loseCount}패")
playerGameResults.forEach { println("${it.name}: ${convertResultToMessage(it.result)}") }
}

private fun convertResultToMessage(result: PlayerWinLoseResult): String {
return when (result) {
PlayerWinLoseResult.WIN -> "승"
PlayerWinLoseResult.LOSE -> "패"
PlayerWinLoseResult.PUSH -> "무"
}
println("딜러: ${dealerResult.getEarnAmount()}")
playerGameResults.forEach { println("${it.name}: ${it.getEarnAmount()}") }
}

private fun printDealerResult(dealer: Dealer) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/view/PlayerInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.view

data class PlayerInfo(
val name: String,
val amount: Double,
)
12 changes: 6 additions & 6 deletions src/test/kotlin/blackjack/BlackJackGameTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,36 @@ import io.kotest.matchers.types.shouldBeInstanceOf

class BlackJackGameTest : StringSpec({
"게임에 참여한 유저들의 정보와 한명의 딜러의 정보를 가지고 있다." {
val game = BlackJackGame.createGame(createPlayers(2), RandomDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), RandomDeck())

game.players.members.size shouldBe 2
game.dealer.shouldBeInstanceOf<Dealer>()
}

"게임이 시작되면 각 유저들은 2개의 패를 지급받는다." {
val game = BlackJackGame.createGame(createPlayers(2), RandomDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), RandomDeck())

game.players.members.forEach {
it.hand.cards.size() shouldBe 2
}
}

"유저들에게 최초 지급된 패가 21일 경우 블랙잭을 선언한다." {
val game = BlackJackGame.createGame(createPlayers(2), aceDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), aceDeck())

game.checkBlackJack() shouldBe true
}

"유저에게 패를 한장 지급할 수 있다." {
val game = BlackJackGame.createGame(createPlayers(2), RandomDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), RandomDeck())
val currentPlayer = game.currentPlayer
game.hit(currentPlayer)

currentPlayer.hand.cards.size() shouldBe 3
}

"유저가 더이상 패를 지급하지 못하도록 상태를 변경할 수 있다." {
val game = BlackJackGame.createGame(createPlayers(2), RandomDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), RandomDeck())
val currentPlayer = game.currentPlayer
game.stay(currentPlayer)

Expand All @@ -47,7 +47,7 @@ class BlackJackGameTest : StringSpec({
}

"딜러의 패가 16이하일 경우 한장을 뽑도록 한다." {
val game = BlackJackGame.createGame(createPlayers(2), basicDeck())
val game = BlackJackGame.createGame(createPlayerInfos(2), basicDeck())

game.handleDealerChance()

Expand Down
5 changes: 3 additions & 2 deletions src/test/kotlin/blackjack/HelperFunction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import blackjack.domain.Card
import blackjack.domain.CardMark
import blackjack.domain.CardNumber
import blackjack.domain.Deck
import blackjack.view.PlayerInfo

fun createAceCard(): Card {
return Card(CardNumber.ACE, CardMark.HEART)
Expand All @@ -16,8 +17,8 @@ fun createBasicCard(
return Card(number, mark)
}

fun createPlayers(number: Int): List<String> {
return (1..number).map { "name$it" }
fun createPlayerInfos(number: Int): List<PlayerInfo> {
return (1..number).map { PlayerInfo("name$it", 1000.0) }
}

private val CACHED_CARDS =
Expand Down
21 changes: 21 additions & 0 deletions src/test/kotlin/blackjack/PlayerGameResultTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackjack

import blackjack.domain.BetAmount
import blackjack.domain.PlayerGameResult
import blackjack.domain.PlayerWinLoseResult
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import kotlin.math.roundToInt

class PlayerGameResultTest : StringSpec({
"최종 수익을 계산한다." {
// given
val result = PlayerGameResult("player1", BetAmount(10000.0), PlayerWinLoseResult.WIN)

// when
val earnAmount = result.getEarnAmount()

// then
earnAmount shouldBe (10000.0 * 1.0).roundToInt()
}
})
Loading