-
Notifications
You must be signed in to change notification settings - Fork 82
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
base: cucumber99
Are you sure you want to change the base?
Changes from all commits
3c4b623
a78cd53
d375814
ea2d4ce
6eff76f
5ac1e8b
703f36c
b8c58c0
c35b239
6755536
49b2d73
5873be5
a51d898
29f574f
573fcf8
813cf7d
abddffb
6a4d227
36adbad
4b3951b
bc1a9d5
423fbe6
5e93d95
c51bb9c
6237de5
a685667
c4f7720
7d2ea03
829b055
c45ee14
79608dd
2908512
b6706fc
a669436
b960995
41aa768
f5f2b42
de33c2f
9e29f3e
f47d721
8ecff96
c87c72c
82e8d16
aaa0837
d86836b
49dc7d9
0585790
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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] 딜러의 점수와 각 플레이어의 점수를 비교하여 승패 여부를 판단한다 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package blackjack | ||
|
||
import blackjack.controller.BlackJackController | ||
|
||
fun main() { | ||
BlackJackController().play() | ||
} |
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 | ||
} |
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) | ||
} | ||
} |
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) | ||
}, | ||
) | ||
} | ||
} | ||
} |
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 | ||
} | ||
} |
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) | ||
} | ||
} | ||
} |
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), | ||
} |
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, | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
우선 저는 대체로 초기값을 전달해주는 편이에요. 이것만으로도 테스트가 쉬워진다고 생각하거든요. 카드를 전달해주거나, 카드 생성 방식을 전달해주거나요! 예시는 여러가지가 있을 것 같아요. |
||
|
||
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 = "남은 카드가 없습니다." | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 부생성자가 필요할까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분이 위에서 언급했던 점입니다. |
||
|
||
override fun draw(deck: Deck) { | ||
val amount = getDrawAmount(DealerState.FIRST_TURN) | ||
repeat(amount) { | ||
hand.addCard(deck.draw()) | ||
} | ||
gameState = DealerState.from(this) | ||
} | ||
} |
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) | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. state가 같으면 2장, 같지 않으면 1장 이런 로직을 갖고 있는데요, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 상태에 따른 분기 처리, 예외 처리까지 할 수 있을 것 같아요. |
||
} |
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 | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
팩토리 함수가 생성자랑 동일한 역할을 하는 것으로 보여요!