Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Step3 블랙잭(딜러) #785

Open
wants to merge 15 commits into
base: duhanmo
Choose a base branch
from
Open
Changes from 14 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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -20,4 +20,21 @@
- [x] 덱에서 카드를 하나 꺼낸다.
- [x] 유저는 카드 목록을 보유한다.
- [x] 카드목록에서 점수를 계산한다.
- [x] 카드를 덱에서 하나 뽑는다.
- [x] 카드를 덱에서 하나 뽑는다.

## Step3 - 블랙잭(딜러)

### Step2 - 리뷰반영사항
- [x] BlackJackGame::game table 이 users 와 deck 을 상태로서 관리하도록 수정
- [x] BlackJackGame::start 메서드 분리
- [x] BlackJackGame::유저목록을 받아 card 출력하도록 수정
- [x] BlackJackGame::카드 히트 여부확인시 출력과 입력을 통합
- [x] BlackJackGame::while 문 내부 if 절을 while 조건식으로 통합
- [x] BlackJackGame::게임진행책임 분리
- [x] BlackJackGame::receive 를 hit 로 메서드명 수정
- [x] Rank::enum 클래스로 수정

### 기능 구현사항
- [x] 딜러는 처음받는 2장의 합계가 16이하면 반드시 1장의 카드를 추가로 받는다
- [x] 딜러가 21을 초과하면 남은 플레이어들은 패에 상관없이 승리한다
- [x] 게임 완료 후 각 플레이어별로 승패를 출력한다
5 changes: 2 additions & 3 deletions src/main/kotlin/blackjack/Main.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package blackjack

import blackjack.controller.BlackJackGame
import blackjack.domain.GameTable
import blackjack.controller.BlackjackGame
import blackjack.view.InputView
import blackjack.view.ResultView

fun main() {
BlackJackGame(GameTable, InputView, ResultView).start()
BlackjackGame(InputView, ResultView).start()
}
50 changes: 0 additions & 50 deletions src/main/kotlin/blackjack/controller/BlackJackGame.kt

This file was deleted.

87 changes: 87 additions & 0 deletions src/main/kotlin/blackjack/controller/BlackjackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package blackjack.controller

import blackjack.domain.Dealer
import blackjack.domain.Deck
import blackjack.domain.GameResult
import blackjack.domain.GameTable
import blackjack.domain.Participant
import blackjack.domain.Player
import blackjack.view.InputView
import blackjack.view.ResultView

data class BlackjackGame(

Choose a reason for hiding this comment

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

BlackjackGame을 data class 로 정의한 이유가있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

일관성을 맞추기 위해 특별한 경우가 아니면 data class 로 선언을 했는데요,
좀더 의미를 가지고 정의를 하도록 할게요🙂

--
추가 반영하며 data class와 일반 class를 나눈 근거는
객체가 직접 자신의 상태를 변경하며 관리하는 클래스는 일반 class,
내부 로직이 없으며 데이터로서의 역할을 하는 클래스는 data class로 선언하였어요!

private val inputView: InputView,
private val resultView: ResultView,
) {
fun start() {
val gameTable = GameTable(Deck.create())
val participants = playGame(gameTable)
printCard(participants)
printGameResult(participants)
}

private fun playGame(gameTable: GameTable): List<Participant> {
val participants = setUpInitCard(gameTable)
val (players, dealer) = Participant.separate(participants)

Choose a reason for hiding this comment

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

딜러와 플레이어를 분리해서 사용한다면,
애초에 participants로 하나로 합칠 필요가 있을까요?
일부분에서는 participants를 통해 함께 관리하고,
일부분이서는 separate를 통해 따로 관리하는 구조는 일관성이 조금 없는거 같단 생각이 들어요 :)

Copy link
Author

Choose a reason for hiding this comment

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

네! 피드백 반영하며 수정하였어요😀 플레이어와 딜러의 상위클래스를 따로 두지 않고 플레이어의 기능만을 상속받는 딜러를 구현하였어요😊

val gamedPlayers = playersTurn(players, gameTable)
resultView.linebreak()
val gamedDealer = dealerTurn(dealer, gameTable)
return gamedPlayers + gamedDealer
}

private fun setUpInitCard(gameTable: GameTable): List<Participant> {
val participants = gameTable.dealInitCard(getParticipants())
resultView.linebreak()
resultView.printInitCardReceive(participants)
resultView.printParticipantsCard(participants = participants, printScore = false)
resultView.linebreak()

Choose a reason for hiding this comment

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

resultView에게 책임을 전달해주면 어떨까요?
resultView를 사용하여 print하는게 아니라
resultView에게 책임을 주어 메세지를 던져서 일을 시키는 구조를 고민해보면 좋을거같아요 :)

View가 콘솔뷰가 아니라, 웹이나 모바일 화면이라면
View와 의존성이 크지 않은 Controller의 로직들도 많이 바뀌어야하진 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

네! 반영하도록 할게요😄

return participants
}

private fun getParticipants(): List<Participant> {
return buildList {
add(Dealer.create())
addAll(inputView.inputNames().map { Player.create(name = it) })
}
}

private fun playersTurn(
participants: List<Participant>,
gameTable: GameTable,
): List<Participant> {
return participants.map { playerTurn(it, gameTable) }
}

private tailrec fun playerTurn(
player: Participant,
gameTable: GameTable,
): Participant {
if (!player.canHit() || !inputView.inputHit(player)) {
return player
}
val hitPlayer = gameTable.hit(player)
resultView.printParticipantCard(participant = hitPlayer, printScore = false)
return playerTurn(hitPlayer, gameTable)
}

private tailrec fun dealerTurn(
dealer: Participant,
gameTable: GameTable,
): Participant {
if (!dealer.canHit()) {
return dealer
}
resultView.printDealerHit()
return dealerTurn(gameTable.hit(dealer), gameTable)
}

private fun printCard(participants: List<Participant>) {
resultView.linebreak()
resultView.printParticipantsCard(participants = participants, printScore = true)
}

private fun printGameResult(participants: List<Participant>) {
resultView.linebreak()
resultView.printGameResult(GameResult.from(participants))

Choose a reason for hiding this comment

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

해당 로직들은 Controller보다는 View의 책임은 아닐까요?

Copy link
Author

Choose a reason for hiding this comment

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

네! View에 위임하도록 할게요👍

}

Choose a reason for hiding this comment

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

BlackjackGame은 Controller역할을 하고 있어요,
하지만 BlackjackGame이 너무 비대하진 않을까요?
너무 많은 로직이 있는거 같아요
책임을 분리해보아요!

Copy link
Author

Choose a reason for hiding this comment

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

네! 말씀대로 GameTable을 service로직처럼 상위패키지(controller)로 추출하여 로직을 분담하도록했어요🙂

}
12 changes: 6 additions & 6 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package blackjack.domain

import blackjack.domain.Rank.Companion.ACE
import blackjack.domain.Rank.ACE

data class Card(
val rank: Rank,
val suit: Suit,
) {
val score = rank.score
val score: Int
get() = rank.score

fun isAce(): Boolean {
return rank == ACE
}
val isAce: Boolean
get() = rank == ACE

companion object {
val ALL: List<Card> =
Suit.entries.flatMap { suit -> Rank.ALL.map { rank -> Card(rank, suit) } }
Suit.entries.flatMap { suit -> Rank.entries.map { rank -> Card(rank, suit) } }
}
}
21 changes: 18 additions & 3 deletions src/main/kotlin/blackjack/domain/Cards.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
package blackjack.domain

import blackjack.domain.MatchResult.DRAW
import blackjack.domain.MatchResult.LOSS
import blackjack.domain.MatchResult.WIN

data class Cards(val values: List<Card>) {
val score: Int
get() = calculateScore()

fun isScoreLowerThanLimit(): Boolean {
return score < BLACKJACK_SCORE_LIMIT
val isBust: Boolean
get() = calculateScore() > BLACKJACK_SCORE_LIMIT

fun scoreLowerThan(limit: Int): Boolean {
return score < limit
}

fun compareScore(other: Cards): MatchResult {
return when {
score > other.score -> WIN
score < other.score -> LOSS
else -> DRAW
}

Choose a reason for hiding this comment

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

점수가 같더라도, 블랙잭 여부에 따라서 승패가 나눠질수도 있어요!

처음 받은 2장 합쳐 21이 나오는 경우 블랙잭이 되며

https://namu.wiki/w/%EB%B8%94%EB%9E%99%EC%9E%AD(%ED%94%8C%EB%A0%88%EC%9E%89%20%EC%B9%B4%EB%93%9C)

Copy link
Author

Choose a reason for hiding this comment

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

네! 해당부분도 고려하도록 할게요🙂

}

fun add(card: Card): Cards {
@@ -14,7 +29,7 @@ data class Cards(val values: List<Card>) {

private fun calculateScore(): Int {
val totalScore = values.sumOf { it.score }
var aceCount = values.count { it.isAce() }
var aceCount = values.count { it.isAce }

var adjustedScore = totalScore
while (adjustedScore > BLACKJACK_SCORE_LIMIT && aceCount > 0) {
27 changes: 27 additions & 0 deletions src/main/kotlin/blackjack/domain/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package blackjack.domain

import blackjack.domain.MatchResult.DRAW
import blackjack.domain.MatchResult.LOSS
import blackjack.domain.MatchResult.WIN
import blackjack.domain.dto.DealerGameResult
import blackjack.domain.dto.PlayerGameResult

data class GameResult(
val dealerGameResult: DealerGameResult,
val playerGameResults: List<PlayerGameResult>,
) {
companion object {
fun from(participants: List<Participant>): GameResult {
val (players, dealer) = Participant.separate(participants)
val playerGameResults = players.map { player -> PlayerGameResult(player, player.compareScore(dealer)) }
return GameResult(
DealerGameResult(
winCount = playerGameResults.count { it.result == LOSS },
lossCount = playerGameResults.count { it.result == WIN },
drawCount = playerGameResults.count { it.result == DRAW },
),
playerGameResults,
)
}
}
}
25 changes: 15 additions & 10 deletions src/main/kotlin/blackjack/domain/GameTable.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package blackjack.domain

object GameTable {
const val INIT_CARD_DRAW_COUNT = 2

fun dealInitCard(
users: List<User>,
deck: Deck,
): List<User> {
return users.map { user ->
(1..INIT_CARD_DRAW_COUNT).fold(user) { acc, _ ->
acc.receiveCard(deck.draw())
data class GameTable(
private val deck: Deck,
) {

Choose a reason for hiding this comment

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

게임테이블에서 참가자목록을 갖고있도록 했었는데,
참가자가 hit할때마다 새로운 참가자로 카피되어서 나오다보니, 게임테이블에서 갖고있게 되는(최초생성할 때 인자로넣어주는) 참가자목록은 변하지않고 계속 hit 하지 않은 최초 참가자가 되더라구요.
그래서 참가자를 따로 메서드에서 인자로받아 처리하도록 수정했어요.

먼저 게임테이블에서 참가자목록을 가진 불변 컬렉션으로 만들었다면,
게임 진행될때마다, 새로운 상태의 게임테이블을 반환하게 할수도 있고,
게임테이블에서 유저의 상태를 관리하는 책임을 가질수도 있을거같네요 :)

이렇게 하다보니 불변성을 지키려할수록 객체지향스러운가? 싶은 고민들이 생겨났어요🙄 (특히, Cards 입장에서 hit 할 때마다 새로운 Cards 객체를 생성해서 반환하는 부분) 이부분에 대한 남재님의 의견이 특히 궁금해요😄

불변성과 객체지향은 조금 다른 이야기라고 볼수 있어요 :)
객체지향이 항상 불변성을 가진것도 아니고, 불변성이 항상 객체지향을 의미하지 않습니다!
불변성과 객체지향 서로 다르지만, 서로 대립되는 개념 또한 아니기도합니다.
어떻게 보면 불변성은 함수형프로그래밍에서 강조되는 부분이기도하고,
객체지향에서 객체내 상태가 변경되는것도 자연스럽다고 볼수도 있어요 :)

각각의 장단점을 고려하면서, 요구사항에 따라서 적절히 조화를 하는게 가장 이상적인 방법은 아닐까요?
코드에는 정답이 없으니, 두한님만의 규칙과 철학을 고민해보면 어떨까요?

객체가 상태를 관리해야하는 비즈니스로직이 있다면, 객체가 내부적으로 상태관리를 해주는게 맞지는않을까요?
개인적으로는 결국 BlackJackGame 내에서는 Controller역할 이외에도 블랙잭 게임의 책임을 가지고있고,
참가자들에 대한 상태 관리 책임까지도 가지고 있는건 아닐까요?

fun dealInitCard(participants: List<Participant>): List<Participant> {
return participants.map { participant ->
(1..INIT_CARD_DRAW_COUNT).fold(participant) { acc, _ ->
acc.hit(deck.draw())
}
}
}

fun hit(participant: Participant): Participant {
return participant.hit(deck.draw())
}

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

enum class MatchResult(val description: String) {
WIN("승"),
LOSS("패"),
DRAW("무"),
}
63 changes: 63 additions & 0 deletions src/main/kotlin/blackjack/domain/Participant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package blackjack.domain

import blackjack.domain.MatchResult.LOSS
import blackjack.domain.MatchResult.WIN

sealed class Participant(val name: String, val cards: Cards) {

Choose a reason for hiding this comment

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

일반적으로 코틀린에서는 클래스별로 하나의 파일에서 관리되도록 가이드하고 있긴합니다 :)
이부분은 스타일마다 다를수 있으니, 참고만해주세요 :)

Copy link
Author

Choose a reason for hiding this comment

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

네! 조언 감사합니다🙇‍♂️

val isBust: Boolean
get() = cards.isBust

abstract fun canHit(): Boolean

abstract fun hit(card: Card): Participant

companion object {
fun separate(participants: List<Participant>): Pair<List<Player>, Dealer> {
return participants.filterIsInstance<Player>() to participants.first { it is Dealer } as Dealer
}
}
}

class Player(name: String, cards: Cards) : Participant(name, cards) {
override fun canHit(): Boolean {
return cards.scoreLowerThan(PLAYER_SCORE_LIMIT)
}

override fun hit(card: Card): Player {
return Player(this.name, cards.add(card))
}

fun compareScore(dealer: Dealer): MatchResult {
return when {
dealer.isBust -> WIN
this.isBust -> LOSS
else -> cards.compareScore(dealer.cards)
}
}

Choose a reason for hiding this comment

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

승패를 결정하는 로직이 Cards, Player 객체에 혼재되어있는건 아닐까요?
책임을 하나의 객체로 위임해보면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

네! 해당 결정로직이 카드까지 흘러들어가지 않고 (반영후에는 Hand 클래스) 플레이어가 판단하도록 수정하였어요😊


companion object {
private const val PLAYER_SCORE_LIMIT = 21

fun create(name: String): Player {
return Player(name, Cards(emptyList()))
}
}
}

class Dealer(cards: Cards) : Participant("딜러", cards) {
override fun canHit(): Boolean {
return cards.scoreLowerThan(DEALER_SCORE_LIMIT)
}

override fun hit(card: Card): Dealer {
return Dealer(cards.add(card))
}

companion object {
private const val DEALER_SCORE_LIMIT = 17

fun create(): Dealer {
return Dealer(Cards(emptyList()))
}
}
}
Loading