Skip to content

Commit

Permalink
[부나] 1단계 블랙잭 제출합니다. (#29)
Browse files Browse the repository at this point in the history
* chore: .gitkeep 파일 제거

* study: DSL 스터디

* docs: 기능 목록 요구사항 작성

* refactor: DslTest.kt 패키지 이동

* feat: 스페이드, 다이아몬드, 하트, 클로버를 가지는 카드 모양 클래스 구현

* feat: 카드 숫자의 범위가 1부터 13인지 검증하는 기능 구현

* feat: 카드 숫자를 문자로 반환하는 기능 구현

* refactor: 카드 모양 클래스 요소를 한글명으로 변경

* feat: 카드는 각 모양별로 2부터 10, A, J, Q, K가 존재하도록 구현

* feat: 카드 목록에 카드를 추가하는 기능 구현

* feat: 카드 숫자를 정수형으로 반환하는 기능 구현

* feat: 총 점수를 반환하는 기능 구현

* refactor: 카드 목록 클래스명 변경

* feat: 카드 하나를 뽑는 기능 구현

* feat: 이름을 갖는 플레이어 클래스 구현

* feat: 플레이어가 가진 카드 목록에 카드를 추가하는 기능 구현

* feat: 카드의 합이 21을 초과하지 않는지 확인하는 기능 구현

* feat: 플레이어 자신의 점수를 반환하는 기능 구현

* refactor: 플레이어의 카드 목록 블랙잭 상수 캡슐화

* feat: 플레이어가 자신이 가진 카드를 반환하는 기능 구현

* feat: 모든 플레이어의 카드를 2장씩 뽑는 기능 구현

* feat: 딜러가 스테이인지 확인하는 기능 구현

* feat: 플레이어의 승패를 결정하는 기능 구현

* feat: 모든 플레이어의 승패 결과를 반환하는 기능 구현

* feat: 사용자의 입력을 받는 기능 구현

게임에 참여하는 플레이어의 이름을 받는다.
카드를 더 받을지 알려주는 명령어를 받는다.

* feat: 게임 참여자의 초기 카드 세팅을 출력하는 기능 구현

* feat: 딜러가 카드를 한 장 더 받는 메세지를 출력하는 기능 구현

* refactor: 게임 참여자들이 가지고 있는 정보 DTO 리팩터링

* feat: 스코어 보드 출력하는 기능 구현

* refactor: 게임 참여자의 초기 카드 세팅을 출력하는 기능 리팩터링

* refactor: 최종 결과를 출력하는 기능 구현

* feat: 블랙잭 초기 설정 구현

* feat: 플레이어에게 카드 뽑을지 물어보는 기능 구현

* feat: 딜러 차례에 카드를 추가로 뽑는 기능 구현

* feat: 스코어 보드 출력 기능 컨트롤러와 연결

* feat: 최종 승패 출력 기능 컨트롤러와 연결

* feat: 명령어 검증 기능 구현

* test: 딜러는 자신이 보유한 첫번째 카드를 반환하는 기능 테스트 추가

* refactor: 코드 라인 및 depth 줄임

* refactor: 카드 모양 네이밍 변경

- Shape보다 명확한 Suit로 변경
- enum 요소를 한글에서 영어로 변경

* refactor: 카드 숫자를 enum 클래스로 변경

* refactor: 카드덱 Generator 제거

- 카드덱과 Generator가 서로 다른 카드덱을 가지는 문제 해결.
- 카드를 뽑을 때마다 카드덱을 셔플할 필요가 없다.
- 카드 중복 여부를 판단할 필요가 없다.

* refactor: 카드 모양 열거 순서 변경

* refactor: 카드 클래스 생성 방식 변경

- 팩토리 메서드에서 id를 전달받아 생성하는 대신, 카드 모양과 카드 번호를 받아서 생성자로 생성하도록 변경

* refactor: 점수 계산 로직 수정

1. 카드 점수를 합산한다
2. 에이스 카드가 있다면, 보너스 점수 10점을 더한 결과가 21보다 작다면 그 값을 반환한다.

* refactor: 입출력 상수 리팩터링

1. 에러메시지 인라인으로 변경
2. Separator, delimiter 상수화

* feat: 플레이어 수를 제한하는 기능 구현

* refactor: 카드 뽑기 명령어 검증을 View에서 처리하도록 변경

* refactor: 블랙잭 객체를 컨트롤러의 시작 함수 내에서 초기화하도록 변경

* refactor: 딜러와 플레이러가 참여자 클래스를 상속받도록 변경

* refactor: 딜러가 승부 결과를 반환하도록 변경

* refactor: 참여자가 처음 카드를 공개하는 로직을 추상화

* refactor: 이름과 보유한 카드 목록을 반환하는 기능 리팩터링

* refactor: 플레이어가 보유한 카드를 처음 오픈하는 기능에서 DTO 제거

* refactor: 게임 참여자의 점수를 반환하는 기능에서 DTO 제거

* refactor: 카드를 디스플레이 하는 기능을 도메인 로직에서 UI 로직으로 이동

* refactor: 도메인에서 DSL 제거

* refactor: 카드 목록에서 첫 번째 카드를 가져오는 함수의 반환 타입 변경

- List<Card>에서 Card로 변경

* fix: 플레이어가 카드를 뽑을 수 있는지 여부를 반환하는 조건 수정

* refactor: 참여자끼리 승패를 반환하는 기능을 참여자 클래스로 이동

* feat: 참여자의 스테이 상태를 반환하는 기능 구현

* refactor: 딜러와 플레이어들을 참여자들을 관리하는 클래스로 이동

* refactor: 참여자들이 카드를 뽑는 과정을 함수형으로 변경

* refactor: 게임 결과를 합쳐서 블랙잭 클래스에서 반환하도록 변경

* refactor: 게임 결과를 합쳐서 블랙잭 클래스에서 반환하도록 변경

* refactor: 카드의 총 점수를 계산하는 기능에서 블랙잭 점수를 클래스 내에서 참조하도록 변경

* refactor: 카드 목록에 카드를 가변 인자로 추가할 수 있도록 변경

* refactor: 패키지, 클래스, 메서드명에서 BlackJack을 Blackjack으로 변경

* refactor: 카드덱에 가변인자를 전달받는 부생성자 추가

* refactor: 카드목록의 초기화 블록의 코드 위치 변경

* test(카드 목록): 카드 목록 테스트 코드 추가

* test(카드): 카드 테스트 코드에서 각 테스트 케이스별로 테스트명을 출력하도록 변경

* fix: 딜러가 카드를 뽑을 수 있는지 판단하는 메서드 버그 수정

* test(딜러): 딜러와 플레이어의 중복 테스트 코드 제거

* refactor: 사용하지 않는 클래스 제거

* test(카드 모양): 카드 모양 테스트 코드에서 인자 변경

* refactor: 플레이어가 카드를 뽑을 것인지 판단하는 콜백의 기본값 설정

* refactor: 참여자들의 인원 제한 변경

- 최소 인원을 1명에서 2명으로 변경

* refactor: 매 차례뼐로 카드를 뽑는 메서드 분리

* refactor: 참여자가 보유한 카드 점수가 21점이 넘는지 판단하는 메서드 변경

* test(플레이어): 카드를 뽑을 수 있는지 여부를 반환하는 테스트 코드 결과 수정

* refactor: 참여자들 카드를 뽑는 순서를 플레이어와 딜러순으로 분리

* test(참여자들): 참여자들 테스트 코드 추가

* refactor: 카드 결과 출력시 한 칸씩 띄도록 변경

* refactor: 블랙잭 컨트롤러에서 게임 시작 람다 함수를 리플렉션으로 축약

---------

Co-authored-by: SangHyun Moon <[email protected]>
  • Loading branch information
tmdgh1592 and no1msh authored Mar 9, 2023
1 parent ed88432 commit 5e3e342
Show file tree
Hide file tree
Showing 30 changed files with 1,163 additions and 0 deletions.
60 changes: 60 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 기능 요구 사항
- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다.
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.
- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
- 게임을 완료한 후 각 플레이어별로 승패를 출력한다.

# 기능 목록
- 카드 모양
- [x] 카드 모양에는 스페이드, 다이아몬드, 하트, 클로버가 있다.

- 카드 숫자
- [x] 카드 숫자의 범위는 1부터 13이다.
- [x] 카드 숫자를 문자로 반환한다.
- [x] 1, 11, 12, 13은 각각 A, J, Q, K로 반환한다.
- [x] 2 ~ 10은 그대로 반환한다.
- [x] 카드 숫자를 정수형으로 반환한다.

- 카드
- [x] 카드는 각 모양별로 2부터 10, A, J, Q, K가 존재한다.

- 플레이어의 카드 목록
- [x] 카드 목록에 카드를 추가한다.
- [x] 총 점수를 반환한다.
- [x] 단, A는 기존 총 점수에 11점을 더한 값이 21점 이하이면 11점으로 계산하고, 21점을 초과하면 1점으로 계산한다.
- [x] 단, J, Q, K는 모두 10점으로 계산한다.
- [x] 2 ~ 10은 그대로 계산한다.

- 카드덱
- [x] 카드 하나를 뽑는다.
- [x] 카드덱은 아직 뽑히지 않은 카드를 갖는다.
- [x] 뽑은 카드는 아직 뽑히지 않은 카드 중 하나여야 한다.

- 플레이어
- [x] 플레이어는 이름을 갖는다.
- [x] 플레이어는 카드 목록에 카드를 추가한다.
- [x] 플레이어는 자신이 뽑은 카드 목록을 갖는다.
- [x] 플레이어는 카드의 합이 21을 초과하지 않는지 확인한다.
- [x] 자신의 점수를 반환한다.
- [x] 자신이 가진 카드를 반환한다.

- 플레이어들
- [x] 모든 플레이어의 카드를 2장씩 뽑는다.

- 딜러
- [x] 딜러는 스테이인지 확인한다.
- [x] 카드의 합이 17점 이상이면 스테이이다.
- [x] 카드의 합이 16점 이하이면 스테이가 아니다.
- [x] 딜러는 자신이 보유한 첫번째 카드를 반환한다.

- 승부 결과
- [x] 플레이어의 승패를 결정한다.
- [x] 플레이어가 21점을 초과하면 패배한다.
- [x] 딜러만 21점을 초과하면 플레이어가 승리한다.
- [x] 딜러와 플레이어 모두 21점을 초과하지 않고 플레이어가 딜러보다 점수가 높으면 플레이어가 승리한다.
- [x] 딜러와 플레이어 모두 21점을 초과하지 않고 플레이어와 딜러의 점수가 같으면 무승부이다.
- [x] 딜러와 플레이어 모두 21점을 초과하지 않고 딜러가 플레이어보다 점수가 높으면 플레이어가 패배한다.

- 명령어
- [x] 명령어는 y 또는 n이다.
- [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().start()
}
36 changes: 36 additions & 0 deletions src/main/kotlin/blackjack/controller/BlackjackController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package blackjack.controller

import blackjack.domain.Blackjack
import blackjack.domain.CardDeck
import blackjack.domain.Player
import blackjack.view.InputView
import blackjack.view.OutputView

class BlackjackController {
fun start() {
with(initBlackjack()) {
setUpCard(this)

val result = start(onDrawn = OutputView::printDrawn)
OutputView.printBlackjackResult(result)
}
}

private fun initBlackjack(): Blackjack = Blackjack(CardDeck(), enrollPlayers())

private fun enrollPlayers(): List<Player> = InputView.inputNames().map { name ->
Player(name, needToDraw = {
InputView.inputDrawCommand(name)
})
}

private fun setUpCard(blackJack: Blackjack) {
drawInitialCards(blackJack)
OutputView.printFirstOpenCards(blackJack.getFirstOpenCards())
OutputView.printInterval()
}

private fun drawInitialCards(blackJack: Blackjack) {
blackJack.readyToStart()
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/blackjack/domain/Blackjack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackjack.domain

class Blackjack(private val deck: CardDeck, private val participants: Participants) {
constructor(deck: CardDeck, players: List<Player>) : this(deck, Participants(listOf(Dealer()) + players))

fun readyToStart() {
participants.drawFirst(deck)
}

fun start(onDrawn: (Participant) -> Unit): BlackjackResult {
participants.takePlayerTurns(deck, onDrawn)
participants.takeDealerTurns(deck, onDrawn)

return BlackjackResult(
participants.getCardResults(),
participants.getMatchResults(),
)
}

fun getFirstOpenCards(): Map<String, List<Card>> = participants.getFirstOpenCards()
}
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackjackResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain

data class BlackjackResult(
val cardResults: List<CardResult>,
val matchResults: List<MatchResult>,
)
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain

data class Card(val number: CardNumber, val suit: Suit) {
fun getScore(): Int = number.score

fun isAce(): Boolean = number == CardNumber.ACE

companion object {
private val CARDS = Suit.values().flatMap { suit ->
CardNumber.values().map { cardNumber ->
Card(cardNumber, suit)
}
}

fun all(): List<Card> = CARDS.toList()
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/blackjack/domain/CardDeck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackjack.domain

class CardDeck(deck: List<Card> = Card.all().shuffled()) {
constructor(vararg cards: Card) : this(cards.toList())

private val deck: MutableList<Card> = deck.toMutableList()

fun draw(): Card = deck.removeFirst()
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/CardNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain

enum class CardNumber(val score: 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);
}
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/domain/CardResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack.domain

data class CardResult(
val participant: Participant,
val cards: List<Card>,
val scoreSum: Int,
)
39 changes: 39 additions & 0 deletions src/main/kotlin/blackjack/domain/Cards.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package blackjack.domain

class Cards(vararg cards: Card) {
private val _items: MutableList<Card> by lazy { mutableListOf() }
val items: List<Card>
get() = _items.toList()

init {
_items.addAll(cards)
}

fun add(card: Card) {
_items.add(card)
}

fun getFirstCard(): Card = _items.first()

fun calculateTotalScore(): Int {
val score = _items.sumOf(Card::getScore)
return calculateAceScore(score)
}

private fun calculateAceScore(score: Int): Int =
if (hasAce() && !isOverBlackjack(score + BONUS_SCORE)) score + BONUS_SCORE else score

fun isOverBlackjack(): Boolean = calculateTotalScore() > BLACKJACK_SCORE

private fun isOverBlackjack(score: Int): Boolean = score > BLACKJACK_SCORE

fun isStay(): Boolean = calculateTotalScore() >= STAY_SCORE

private fun hasAce(): Boolean = _items.any(Card::isAce)

companion object {
private const val BONUS_SCORE = 10
private const val BLACKJACK_SCORE = 21
private const val STAY_SCORE = 17
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/domain/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack.domain

class Dealer : Participant(DEALER_NAME) {
override fun getFirstOpenCards(): List<Card> = listOf(getFirstCard())

override fun canDraw(): Boolean = !isStay()

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

enum class GameResult {
WIN, DRAW, LOSE;
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/MatchResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain

data class MatchResult(
val participant: Participant,
val winCount: Int,
val loseCount: Int,
val drawCount: Int
)
32 changes: 32 additions & 0 deletions src/main/kotlin/blackjack/domain/Participant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package blackjack.domain

abstract class Participant(val name: String) {
private val cards = Cards()

abstract fun getFirstOpenCards(): List<Card>

abstract fun canDraw(): Boolean

fun getTotalScore(): Int = cards.calculateTotalScore()

fun isBust(): Boolean = cards.isOverBlackjack()

fun isStay(): Boolean = cards.isStay()

infix fun judge(other: Participant): GameResult = when {
isBust() && other.isBust() -> GameResult.DRAW
isBust() -> GameResult.LOSE
other.isBust() -> GameResult.WIN
getTotalScore() == other.getTotalScore() -> GameResult.DRAW
getTotalScore() > other.getTotalScore() -> GameResult.WIN
else -> GameResult.LOSE
}

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

fun getCards(): List<Card> = cards.items

fun getFirstCard(): Card = cards.getFirstCard()
}
84 changes: 84 additions & 0 deletions src/main/kotlin/blackjack/domain/Participants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package blackjack.domain

class Participants(private val participants: List<Participant>) {
init {
require(participants.size in MINIMUM_PARTICIPANTS..MAXIMUM_PARTICIPANTS) {
"블랙잭은 딜러를 포함하여 최소 ${MINIMUM_PARTICIPANTS}명에서 최대 ${MAXIMUM_PARTICIPANTS}명의 플레이어가 참여 가능합니다. (현재 플레이어수 : ${participants.size}명)"
}
}

fun drawFirst(deck: CardDeck) {
participants.forEach { participant ->
participant.addCard(deck.draw())
participant.addCard(deck.draw())
}
}

fun takePlayerTurns(deck: CardDeck, onDrawn: (Participant) -> Unit) {
getPlayers().forEach { participant ->
drawUntilCanDraw(participant, deck, onDrawn)
}
}

fun takeDealerTurns(deck: CardDeck, onDrawn: (Participant) -> Unit) {
drawUntilCanDraw(getDealer(), deck, onDrawn)
}

private fun drawUntilCanDraw(
participant: Participant,
deck: CardDeck,
onDrawn: (Participant) -> Unit
) {
while (participant.canDraw()) {
draw(participant, deck)
onDrawn(participant)
}
}

fun getMatchResults(): List<MatchResult> = listOf(getDealerMatchResult()) + getPlayerMatchResults()

private fun getDealerMatchResult(): MatchResult {
var (win, lose, draw) = Triple(0, 0, 0)
getPlayers().forEach { player ->
when (getDealer() judge player) {
GameResult.WIN -> win++
GameResult.LOSE -> lose++
GameResult.DRAW -> draw++
}
}
return MatchResult(getDealer(), win, lose, draw)
}

private fun getPlayerMatchResults(): List<MatchResult> = getPlayers().map { player ->
var (win, lose, draw) = Triple(0, 0, 0)
when (player judge getDealer()) {
GameResult.WIN -> win++
GameResult.LOSE -> lose++
GameResult.DRAW -> draw++
}
MatchResult(player, win, lose, draw)
}

fun getCardResults(): List<CardResult> = participants.map { participant ->
CardResult(
participant,
participant.getCards(),
participant.getTotalScore()
)
}

fun getFirstOpenCards(): Map<String, List<Card>> = participants.associate { it.name to it.getFirstOpenCards() }

private fun getPlayers(): List<Participant> = participants.filterIsInstance<Player>()

private fun getDealer(): Participant = participants.first { it is Dealer }

private fun draw(participant: Participant, deck: CardDeck) {
participant.addCard(deck.draw())
}

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

class Player(name: String, private val needToDraw: () -> Boolean = { true }) : Participant(name) {
override fun getFirstOpenCards(): List<Card> = getCards()

override fun canDraw(): Boolean = !isBust() && needToDraw()
}
5 changes: 5 additions & 0 deletions src/main/kotlin/blackjack/domain/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package blackjack.domain

enum class Suit {
SPADE, HEART, DIAMOND, CLOVER;
}
Loading

0 comments on commit 5e3e342

Please sign in to comment.