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

[블랙잭 1단계] 오이 미션 제출합니다. #109

Open
wants to merge 47 commits into
base: cucumber99
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3c4b623
chore: 코틀린 DSL 실습
moondev03 Mar 4, 2025
a78cd53
docs(README): 카드/덱 기능 구현 목록 작성
moondev03 Mar 4, 2025
d375814
feat: 숫자와 패턴을 가진 카드 생성 기능 구현
moondev03 Mar 4, 2025
ea2d4ce
feat: 고유한 52장의 카드를 가진 덱 생성 기능 구현
moondev03 Mar 4, 2025
6eff76f
feat: 요청한 수량에 맞게 카드를 뽑는 기능 구현
moondev03 Mar 4, 2025
5ac1e8b
feat: 패에 카드 추가 기능 구현
moondev03 Mar 4, 2025
703f36c
feat: 플레이어 객체 구현
moondev03 Mar 4, 2025
b8c58c0
feat: 패에 들고 있는 카드의 합을 구하는 기능 구현
moondev03 Mar 4, 2025
c35b239
refactor: 플레이어/딜러로 분리
moondev03 Mar 4, 2025
6755536
refactor: Role 제거
moondev03 Mar 4, 2025
49b2d73
feat: 딜러의 점수와 각 플레이어의 점수를 비교하여 승패 여부를 판단하는 기능 구현
moondev03 Mar 4, 2025
5873be5
chore: domain 패키징
moondev03 Mar 4, 2025
a51d898
refactor: 결과 계산 로직 개선
moondev03 Mar 4, 2025
29f574f
feat: 플레이어 GameState 관리 기능 구현
moondev03 Mar 4, 2025
573fcf8
feat: Controller & View 구현
moondev03 Mar 4, 2025
813cf7d
feat: 패에 들고 있는 카드의 점수를 계산하는 기능 구현
moondev03 Mar 5, 2025
abddffb
refactor: Controller에서 게임 상태 관련 기능 분리
moondev03 Mar 5, 2025
6a4d227
fix: Score 계산 로직 수정
moondev03 Mar 5, 2025
36adbad
refactor: 게임 상태 갱신 & draw 로직 통합
moondev03 Mar 5, 2025
4b3951b
refactor: Controller Indent Depth 정리
moondev03 Mar 5, 2025
bc1a9d5
refactor: Player/Dealer State 분리
moondev03 Mar 6, 2025
423fbe6
feat: 덱에 남은 카드가 없는 경우 검증 기능 구현
moondev03 Mar 6, 2025
5e93d95
chore: isCanDraw -> canDraw로 네이밍 변경
moondev03 Mar 6, 2025
c51bb9c
refactor: PlayerState BLACK_JACE 상태 제거
moondev03 Mar 6, 2025
6237de5
test: PlayerTest & DealerTest 추가
moondev03 Mar 6, 2025
a685667
refactor: 점수 계산 기능 분리
cucumber99 Mar 6, 2025
c4f7720
refactor: draw 로직 개선
cucumber99 Mar 6, 2025
7d2ea03
refactor: 점수 계산 로직 개선
cucumber99 Mar 6, 2025
829b055
refactor: Hand 클래스 개선
cucumber99 Mar 6, 2025
c45ee14
refactor: 변수명 변경
cucumber99 Mar 6, 2025
79608dd
refactor: 드로우 수량 로직 분리
cucumber99 Mar 6, 2025
2908512
refactor: 디렉터리 구조 변경
cucumber99 Mar 6, 2025
b6706fc
test: 딜러 상태 열거형 클래스 테스트
cucumber99 Mar 6, 2025
a669436
test: 플레이어 상태 열거형 클래스 테스트
cucumber99 Mar 6, 2025
b960995
refactor: 승패 여부 로직 조건 추가
cucumber99 Mar 6, 2025
41aa768
feat: PersonUiModel 구현
cucumber99 Mar 6, 2025
f5f2b42
feat: ResultUiModel 구현
cucumber99 Mar 6, 2025
de33c2f
refactor: OutputView 개선
cucumber99 Mar 6, 2025
9e29f3e
refactor: InputView 개선
cucumber99 Mar 6, 2025
f47d721
refactor: draw 메소드 추상화
cucumber99 Mar 7, 2025
8ecff96
refactor: copy 로직 개선
cucumber99 Mar 7, 2025
c87c72c
style: 불필요한 개행 제거
cucumber99 Mar 7, 2025
82e8d16
refactor: 출력 로직 추가
cucumber99 Mar 7, 2025
aaa0837
refactor: 결과 관련 코드 개선
cucumber99 Mar 7, 2025
d86836b
refactor: CardPattern value 제거
cucumber99 Mar 7, 2025
49dc7d9
refactor: Hand 객체 불변성 유지하도록 변경
moondev03 Mar 7, 2025
0585790
refactor: 불필요한 import 제거
cucumber99 Mar 7, 2025
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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# kotlin-blackjack
# kotlin-blackjack


## 기능 구현 목록

### 카드
- [x] 숫자와 패턴을 가진 카드를 생성한다
- 패턴은 클로버/하트/다이아/스페이드

<br>

### 덱
- [x] 고유한 52장의 카드를 가지고 있어야 한다
- [x] 요청한 수량에 맞게 카드를 뽑을 수 있다

<br>

### 플레이어
- [x] 이름과 패, 역할을 가지고 있다
- [x] 덱에서 카드를 뽑아서 패에 가져온다
- [x] 게임 상태를 가지고 있다
- `STAY`/`BUST`/`HIT`/`BLACKJACK`

<br>

### 패
- [x] 패에 카드를 추가할 수 있다

<br>

### 점수 계산기
- [x] 패에 들고 있는 카드의 합을 구할 수 있다

<br>

### 최종 승패
- [x] 딜러의 점수와 각 플레이어의 점수를 비교하여 승패 여부를 판단한다
Empty file removed src/main/kotlin/.gitkeep
Empty file.
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack

import blackjack.controller.BlackJackController

fun main() {
BlackJackController().play()
}
12 changes: 12 additions & 0 deletions src/main/kotlin/blackjack/const/GameRule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package blackjack.const

object GameRule {
const val FIRST_TURN_DRAW_AMOUNT = 2
const val HIT_DRAW_AMOUNT = 1

const val ACE_BASE_SCORE = 10
const val ACE_OTHER_SCORE = 11
const val BLACKJACK_SCORE = 21

const val DEALER_ADDITIONAL_DRAW_BASE_SCORE = 16
}
86 changes: 86 additions & 0 deletions src/main/kotlin/blackjack/controller/BlackJackController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package blackjack.controller

import blackjack.domain.GameResult
import blackjack.domain.card.Deck
import blackjack.domain.person.Dealer
import blackjack.domain.person.Player
import blackjack.uiModel.PersonUiModel
import blackjack.uiModel.ResultUiModel
import blackjack.view.InputView
import blackjack.view.OutputView

class BlackJackController(
private val inputView: InputView = InputView(),
private val outputView: OutputView = OutputView(),
) {
private lateinit var deck: Deck

fun play() {
val players = generatePlayers()
val dealer = Dealer()
deck = Deck()

settingInitialCards(dealer, players)
processPlayerTurns(players)
processDealerTurns(dealer)

showGameResult(dealer, players)
}

private fun generatePlayers(): List<Player> {
outputView.printNameMessage()
return inputView.getNames().map { Player(it) }
}

private fun settingInitialCards(
dealer: Dealer,
players: List<Player>,
) {
dealer.draw(deck)
players.forEach { person -> person.draw(deck) }
outputView.printDrawMessage(combinePerson(dealer, players))
}

private fun processPlayerTurns(players: List<Player>) {
players.forEach { player -> handlePlayerTurn(player) }
}

private fun handlePlayerTurn(player: Player) {
while (player.canDraw()) {
outputView.printFlagMessage(player.name)
letPlayerDrawCard(player)
}
}

private fun letPlayerDrawCard(player: Player) {
if (inputView.getFlag()) {
player.draw(deck)
outputView.printDrawStatus(PersonUiModel.create(player))
return
}
player.changeToStay()
}

private fun processDealerTurns(dealer: Dealer) {
while (dealer.canDraw()) {
outputView.printDealerDrawMessage()
dealer.draw(deck)
}
}

private fun showGameResult(
dealer: Dealer,
players: List<Player>,
) {
outputView.printGameResult(combinePerson(dealer, players))
val gameResult = GameResult.create(dealer, players)
outputView.printResult(ResultUiModel.create(gameResult))
}

private fun combinePerson(
dealer: Dealer,
players: List<Player>,
): List<PersonUiModel> {
return listOf(PersonUiModel.create(dealer)) + players.map(PersonUiModel::create)
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

import blackjack.domain.person.Dealer
import blackjack.domain.person.Player
import blackjack.domain.state.ResultState

class GameResult private constructor(val winStatus: Map<Player, ResultState>) {
companion object {
fun create(
dealer: Dealer,
players: List<Player>,
): GameResult {
return GameResult(
players.associateWith { player ->
ResultState.calculateWin(player, dealer)
},
)
}
}
}
33 changes: 33 additions & 0 deletions src/main/kotlin/blackjack/domain/ScoreCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package blackjack.domain

import blackjack.const.GameRule
import blackjack.domain.card.Card
import blackjack.domain.card.CardNumber

class ScoreCalculator {
fun calculate(cards: List<Card>): Int {
val values = cards.map { getCardValue(it) }
val sum = values.sum()
return adjustAceValues(sum, values)
}

private fun getCardValue(card: Card): Int {
if (card.number == CardNumber.ACE) return GameRule.ACE_OTHER_SCORE
return card.number.value
}

private fun adjustAceValues(
sum: Int,
values: List<Int>,
): Int {
var total = sum
val aceCount = values.count { it == GameRule.ACE_OTHER_SCORE }

repeat(aceCount) {
if (total > GameRule.BLACKJACK_SCORE) {
total -= GameRule.ACE_BASE_SCORE
}
}
return total
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/card/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package blackjack.domain.card

class Card private constructor(
val number: CardNumber,
val pattern: CardPattern,
) {
companion object {
fun create(
number: CardNumber,
pattern: CardPattern,
): Card {
return Card(number, pattern)
}
}
Comment on lines +7 to +14

Choose a reason for hiding this comment

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

팩토리 함수가 생성자랑 동일한 역할을 하는 것으로 보여요!

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

enum class CardNumber(val value: Int) {
ACE(1),

TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),

JACK(10),
QUEEN(10),
KING(10),
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/card/CardPattern.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain.card

enum class CardPattern {
HEART,
CLOVER,
SPADE,
DIAMOND,
}
28 changes: 28 additions & 0 deletions src/main/kotlin/blackjack/domain/card/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackjack.domain.card

class Deck {
private val _cards = mutableListOf<Card>()
val cards: List<Card> get() = _cards.toList()

init {
_cards.addAll(generateDeck())
require(cards.size == DECK_SIZE) { INVALID_DECK_SIZE_ERROR_MESSAGE }
}
Comment on lines +3 to +10

Choose a reason for hiding this comment

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

딜러와 플레이어의 손패 (List) 를 감싸기 위해 Hand라는 일급 컬렉션을 구현했습니다. 또한 Person이라는 추상 클래스를 만들어 딜러와 플레이어 클래스를 구현하고 Person 내부에서 Hand를 관리하는데, 테스트 코드에서 특정 점수에 대한 테스트 등 여러 테스트에서 원하는 대로 카드 목록을 구성하기 위해서 플레이어와 딜러 객체를 생성할 때 Hand 객체를 생성자로 받을 수 있게끔 코드를 작성했습니다. 실제 도메인 로직에서는 Deck에서 카드를 무작위로 생성하여 전달해주기 때문에 위와 같이 코드를 작성했는데, 이 방식과 Deck 자체를 인터페이스로 추상화하여 테스트 코드에서 고정된 카드 목록을 전달해주는 방식 중 어느 방식이 더 바람직한지 둘리의 의견이 궁금합니다.🤔

우선 저는 대체로 초기값을 전달해주는 편이에요. 이것만으로도 테스트가 쉬워진다고 생각하거든요. 카드를 전달해주거나, 카드 생성 방식을 전달해주거나요! 예시는 여러가지가 있을 것 같아요.
지금은 객체를 생성해서 어떤 함수를 실행한 다음에 assert가 돌아가도록 되어있지만, 초기값을 넣을 수 있다면 함수를 실행하지 않아도 되겠죠.
코드 전체적으로 초기값을 생성자에서 받기보다는, 빈 값을 넣고 액션을 하도록 되어있는데요. default parameter를 사용해볼 수도 있고, 빈 값으로 넣어주면 같은 동작을 하니, 각 도메인 모델들의 생성자를 한번 살펴보면 좋겠어요.
어떻게 건드리면 테스트코드가 쉬워질지 눈에 보일거에요!


fun draw(): Card {
require(cards.isNotEmpty()) { NO_SUCH_ELEMENT_ERROR_MESSAGE }
return _cards.removeFirst()
}

private fun generateDeck(): List<Card> = CardPattern.entries.flatMap(::createCard).shuffled()

private fun createCard(cardPattern: CardPattern): List<Card> {
return CardNumber.entries.map { cardNumber -> Card.create(cardNumber, cardPattern) }
}

companion object {
private const val DECK_SIZE = 52
private const val INVALID_DECK_SIZE_ERROR_MESSAGE = "덱은 52장의 카드로 구성되어야 합니다."
private const val NO_SUCH_ELEMENT_ERROR_MESSAGE = "남은 카드가 없습니다."
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/person/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain.person

import blackjack.domain.card.Deck
import blackjack.domain.state.DealerState

class Dealer(hand: Hand) : Person(hand) {
init {
gameState = DealerState.FIRST_TURN
}

constructor() : this(hand = Hand())
Comment on lines +6 to +11

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.

이 부분이 위에서 언급했던 점입니다.
특정 카드들을 플레이어나 딜러가 가지고 있을 때의 상황을 테스트하기 위해서 Hand를 생성자로 전달하거나, 전달하지 않을 경우 내부에서 기본으로 생성하게 됩니다.
때문에 외부에서 부 생성자로 Hand를 받아올 경우 외부와의 참조를 끊기 위해 Handcopy를 선언하고 그대로 사용했습니다.


override fun draw(deck: Deck) {
val amount = getDrawAmount(DealerState.FIRST_TURN)
repeat(amount) {
hand.addCard(deck.draw())
}
gameState = DealerState.from(this)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/blackjack/domain/person/Hand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package blackjack.domain.person

import blackjack.domain.card.Card

class Hand {
private val _cards: MutableList<Card> = mutableListOf()

val cards: List<Card>
get() = _cards.toList()

fun addCard(card: Card) {
_cards.add(card)
}

fun copy(): Hand =
Hand().apply {
_cards.addAll(this@Hand._cards)
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/blackjack/domain/person/Person.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package blackjack.domain.person

import blackjack.const.GameRule
import blackjack.domain.ScoreCalculator
import blackjack.domain.card.Card
import blackjack.domain.card.Deck
import blackjack.domain.state.PersonState

abstract class Person(
hand: Hand,
private val calculator: ScoreCalculator = ScoreCalculator(),
) {
protected lateinit var gameState: PersonState
protected val hand = hand.copy()

abstract fun draw(deck: Deck)

fun cards(): List<Card> = hand.cards

fun canDraw(): Boolean = !gameState.isFinal

fun score(): Int = calculator.calculate(cards())

protected fun getDrawAmount(state: PersonState): Int {
if (gameState == state) {
return GameRule.FIRST_TURN_DRAW_AMOUNT
}
return GameRule.HIT_DRAW_AMOUNT
}
Comment on lines +24 to +29

Choose a reason for hiding this comment

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

state가 같으면 2장, 같지 않으면 1장 이런 로직을 갖고 있는데요, State에 따른 뽑을 수 있는 카드 장수를 선언하면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

상태에 따른 분기 처리, 예외 처리까지 할 수 있을 것 같아요.
좋은 의견 감사합니다😊

}
27 changes: 27 additions & 0 deletions src/main/kotlin/blackjack/domain/person/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package blackjack.domain.person

import blackjack.domain.card.Deck
import blackjack.domain.state.PlayerState

class Player(
val name: String,
hand: Hand,
) : Person(hand) {
init {
gameState = PlayerState.FIRST_TURN
}

constructor(name: String) : this(name = name, hand = Hand())

override fun draw(deck: Deck) {
val amount = getDrawAmount(PlayerState.FIRST_TURN)
repeat(amount) {
hand.addCard(deck.draw())
}
gameState = PlayerState.from(this)
}

fun changeToStay() {
gameState = PlayerState.STAY
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/state/DealerState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain.state

import blackjack.const.GameRule
import blackjack.domain.person.Dealer

enum class DealerState(override val isFinal: Boolean) : PersonState {
FIRST_TURN(false),
HIT(false),
FINISH(true),
;

companion object {
fun from(dealer: Dealer): DealerState =
when {
dealer.cards().size < GameRule.FIRST_TURN_DRAW_AMOUNT -> FIRST_TURN
dealer.score() > GameRule.DEALER_ADDITIONAL_DRAW_BASE_SCORE -> FINISH
else -> HIT
}
}
Comment on lines +12 to +19

Choose a reason for hiding this comment

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

State들이 객체를 받기 보다는 실제 값들(카드 개수, 스코어)을 받아서 판단하면 어떨까요?
여기서 관심사는 사람보다는 사람이 가지고 있는 것들이라고 생각해요!

}
Loading