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단계 블랙잭 제출합니다. #8

Merged
merged 67 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from 65 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9a30725
docs(README) : 기능 구현 목록 작성
re4rk Feb 28, 2023
e3a31d0
feat(CardMark) : 카드들의 문양을 구현한다.
re4rk Feb 28, 2023
7bebf1e
feat(CardValue) : 카드들의 숫자를 구현한다.
re4rk Feb 28, 2023
5e0266b
feat(Card) : 카드들을 구현한다.
re4rk Feb 28, 2023
de3e7b2
feat(CardDeck) : 카드덱을 만든다.
re4rk Feb 28, 2023
ebb3b1b
feat(Cards) : 유저 각자의 카드패를 만든다.
re4rk Feb 28, 2023
99211ec
feat(User) : 딜러랑 유저를 만든다.
re4rk Feb 28, 2023
3505d6f
docs(README) : 기능 구현 목록 수정
re4rk Feb 28, 2023
7240f04
feat(BlackJackGame) : 게임 초기 세팅을 한다.
re4rk Feb 28, 2023
a5e59e7
feat(BlackJackGame) : 중간결과를 반환한다.
re4rk Feb 28, 2023
3890452
feat(BlackJackGame) : 카드의 합을 구한다.
re4rk Mar 1, 2023
018f574
feat(BlackJackGame) : 최종결과를 반환한다.
re4rk Mar 1, 2023
59766ea
feat(InputView) : 사람들의 이름을 입력 받는다.
re4rk Mar 1, 2023
b5a2618
feat(OutputView) : 나눠준 카드를 출력한다.
re4rk Mar 1, 2023
a3a1cf8
feat(InputView) : 카드를 더 받을지 입력 받는다.
re4rk Mar 1, 2023
de0c72f
feat(OutPutView) : 현재 카드 상태를 출력한다.
re4rk Mar 1, 2023
466c804
feat(OutPutView) : 카드의 결과를 반환한다.
re4rk Mar 1, 2023
1bc13ec
feat(BlackJackGame) : 블랙잭 게임 도메인 DSL 적용
re4rk Mar 1, 2023
4764415
refactor(BlackJack) : 매직넘버 삭제
re4rk Mar 1, 2023
30cae0f
refactor(gameStatus) : 필요없는 클래스 정리
re4rk Mar 1, 2023
17b90cb
refactor(Participants) : 인스턴스 분리
re4rk Mar 1, 2023
f5c7282
refactor(BlackJack) : private 처리
re4rk Mar 1, 2023
591d1db
refactor(CardDeck) : 함수명 변경
re4rk Mar 1, 2023
2b0181e
refactor(CardDeck) : 상수처리
re4rk Mar 1, 2023
dd9b03f
refactor(User) : 매직넘버 삭제
re4rk Mar 1, 2023
3e47095
refactor(Participants) : DSL 적용
re4rk Mar 1, 2023
60a6d56
refactor(BlackJackGame) : 확장 함수 적용
re4rk Mar 1, 2023
6f7795c
fix(Test) : 초기화 추가
re4rk Mar 1, 2023
5b8a7b6
refactor(Name) : name 클래스 래핑 추가
re4rk Mar 2, 2023
85a8445
refactor(Participants) : DSL 오브젝트 추가
re4rk Mar 2, 2023
9e96394
refactor(OutputView) : 출력 문양 확장함수 적용
re4rk Mar 2, 2023
528dd60
refactor(OutputView) : 출력 결과 수정
re4rk Mar 2, 2023
0f707d5
refactor(BlackJackGame) : output을 인자로 전달
re4rk Mar 2, 2023
a1e4845
refactor(BlackJackBuilder) : 파생 인자 활용
re4rk Mar 2, 2023
c3e8861
refactor(User) : 선언형 프로그래밍 적용
re4rk Mar 2, 2023
89ee94e
fix(BlackJackGame) : 무한반복 수정
re4rk Mar 2, 2023
5549988
refactor(User) : 딜러 드로우 조건 선언형으로 변경
re4rk Mar 2, 2023
b58161f
refactor(OutputView) : 띄어쓰기 수정
re4rk Mar 2, 2023
aecfaa0
refactor(User) : 유저와 딜러의 스코어 계산 분리
re4rk Mar 2, 2023
d497eb0
test(Test) : DSL 적용
re4rk Mar 2, 2023
5ac413c
fix(BlackJackGame) : 취소 안되는 버그 수정
re4rk Mar 2, 2023
4ac7f57
refactor(OutputView) : 출력 포맷팅
re4rk Mar 3, 2023
aa8ee2f
refactor(OutputView) : 딜러의 이름을 default value 정의
re4rk Mar 3, 2023
7300126
refactor(InputView) : 입력 안내 문구 수정
re4rk Mar 3, 2023
dbc1728
refactor(Name) : 이름의 글자수 제한 추가
re4rk Mar 3, 2023
f4066c2
refactor(BlackJackGame) : 멤버변수 정의 제거 및 커맨드 처리 추가
re4rk Mar 3, 2023
71711db
refactor(Score) : 역할 재부여
re4rk Mar 3, 2023
cd1824f
refactor(User) : 미반영 요구사항 반영
re4rk Mar 3, 2023
8f8cbab
refactor(Outcome) : 확장함수를 적용하여 가독성 개선
re4rk Mar 3, 2023
ff971be
refactor(Card) : Card의 역할을 Cards와 CardDeck으로 분배
re4rk Mar 3, 2023
c410c87
refactor(CardTest) : 테스트 케이스 가독성 개선
re4rk Mar 3, 2023
bf0c973
refactor(CardTest) : 테스트 케이스 가독성 개선
re4rk Mar 3, 2023
8c750d9
refactor(BlackJack) : 클래스 상수 의존성 제거
re4rk Mar 3, 2023
60ed19d
refactor(Builder) : init함수 제거
re4rk Mar 3, 2023
c96c354
feat (study) : 숙제 추가
re4rk Mar 5, 2023
da1b57d
style (BlackJack) : 띄어쓰기 부분개선
re4rk Mar 5, 2023
b1dfd38
feat (BlackJack): 프로퍼티는 상태만 가지도록 변경
re4rk Mar 6, 2023
9cb6249
feat (Cards): Cards 불변객체로 변경
re4rk Mar 6, 2023
1e8a542
feat (Cards): 용도에 맞는 함수이름으로 변경
re4rk Mar 6, 2023
fefb027
feat (BlackJackBuilder): 반환이 필요없는 컬렉션 forEach제거
re4rk Mar 6, 2023
8da9717
feat (BlackJackBuilder): 생성함수 변경
re4rk Mar 6, 2023
93d72d0
feat (CardDeck): 밖에서 셔플한 후에 들어오도록 변경
re4rk Mar 6, 2023
564f955
feat (NameTest): 테스틐 케이스 경계값으로 변경
re4rk Mar 6, 2023
f374a5f
feat (BlackJackGame): 멤버 확장함수 제거
re4rk Mar 6, 2023
95b5e7a
feat (Participants): 함수의 반환타입을 명시적으로 변경
re4rk Mar 6, 2023
e485867
refactor (Blackjack): 패키지별로 파일이동
re4rk Mar 6, 2023
e3844a1
refactor (score): 불변객체로 구현
re4rk Mar 6, 2023
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: 19 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 기능 구현 목록
- [X] 카드들을 구현한다.
- [X] 문양을 구현한다.
- [X] 숫자를 구현한다.
- [X] 카드덱을 만든다.
- [X] 딜러랑 유저를 만든다.
- [X] 유저 각자의 카드덱을 만든다.
- [X] 카드의 합을 구한다.
- [X] 게임 초기 세팅을 한다.
- [X] 중간결과를 반환한다.
- [X] 최종결과를 반환한다.

Choose a reason for hiding this comment

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

다음 사항에 관한 구현 여부를 확인해보세요. 기본적인 기능 요구 사항을 만족하지 않고 있습니다.

  • 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
  • 플레이어는 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. (현재 플레이어 카드가 K, ACE 일 때도 카드를 더 받을 것인지 묻고 있습니다. Ace를 언제 1 또는 11로 계산할지 좀 더 고민이 필요해보여요.)

### 입력 구현 목록
- [X] 사람들의 이름을 입력받는다.
- [X] 카드를 더 받을지 입력 받는다.
### 출력 구현 목록
- [X] 나눠준 카드를 출력한다.
- [X] 현재 카드 상태를 출력한다.
- [X] 카드의 결과를 반환한다.
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package blackjack

import blackjack.controller.BlackJackController
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val blackJackController = BlackJackController(InputView(), OutputView())
blackJackController.run()
}
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.BlackJackGame
import blackjack.domain.Cards
import blackjack.domain.blackJack
import blackjack.view.InputView
import blackjack.view.OutputView

class BlackJackController(
private val inputView: InputView,
private val outputView: OutputView,
) {
fun run() {
val blackJack = setUpBlackJack()
outputView.outputInitState(blackJack)
startBlackJack(blackJack)
outputView.outputResult(blackJack)
}

private fun setUpBlackJack(): BlackJack = blackJack {
cardDeck(Cards.all())
participants {
dealer()
guests(inputView.inputParticipants())
}
draw()
}

private fun startBlackJack(blackJack: BlackJack) =
BlackJackGame().apply {
getCommand = inputView::inputDrawMore
guestsTurn(blackJack.guests, blackJack.cardDeck, outputView::outputCard)
dealerTurn(blackJack.dealer, blackJack.cardDeck, outputView::outputDealerDraw)
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.domain

import blackjack.domain.Outcome.Companion.winTo

data class BlackJack(

Choose a reason for hiding this comment

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

3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

위 요구 사항을 지켜보면 어떨까요?

val cardDeck: CardDeck,
val participants: Participants,
) {
val dealer: Dealer
get() = participants.dealer

val guests: List<Guest>
get() = participants.guests

fun getResult(): List<Outcome> = participants.guests.map { guest -> guest.winTo(participants.dealer) }
}
24 changes: 24 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package blackjack.domain

fun blackJack(block: BlackJackBuilder.() -> Unit): BlackJack {
return BlackJackBuilder().apply(block).build()
}

class BlackJackBuilder {
private lateinit var cardDeck: CardDeck
private lateinit var participants: Participants
fun cardDeck(cards: List<Card>) { cardDeck = CardDeck(cards.shuffled()) }

fun participants(block: ParticipantsBuilder.() -> Unit) {
participants = ParticipantsBuilder().apply(block).build()
}

fun draw() = participants.all().forEach {
it.draw(cardDeck.nextCard())
it.draw(cardDeck.nextCard())
}

fun build(): BlackJack {
return BlackJack(cardDeck, participants)
}
}
38 changes: 38 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package blackjack.domain

class BlackJackGame {
lateinit var getCommand: (String) -> String

fun guestsTurn(guests: List<Guest>, cardDeck: CardDeck, output: (User) -> Unit) =
guests.forEach { guest -> guestTurn(guest, cardDeck, output) }

fun dealerTurn(dealer: Dealer, cardDeck: CardDeck, output: () -> Unit) {
if (dealer.isBlackJack) return
if (dealer.isContinue) {
dealer.draw(cardDeck.nextCard())
output()
}
}

private fun guestTurn(guest: Guest, cardDeck: CardDeck, output: (User) -> Unit) {
if (guest.isBlackJack) return
when (getCommand(guest.name.toString())) {
in DRAW_COMMANDS -> draw(guest, cardDeck, output)
in END_TURN_COMMANDS -> output(guest)
else -> this.guestTurn(guest, cardDeck, output)
}
}

private fun draw(guest: Guest, cardDeck: CardDeck, output: (User) -> Unit) {
guest.draw(cardDeck.nextCard())
output(guest)
if (guest.isContinue) {
guestTurn(guest, cardDeck, output)
}
}

companion object {
private val DRAW_COMMANDS = listOf("Y", "y")
private val END_TURN_COMMANDS = listOf("N", "n")
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package blackjack.domain

data class Card(val mark: CardMark, val value: CardValue)
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/CardDeck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

class CardDeck(cards: List<Card>) {
private val cards: MutableList<Card> = cards.toMutableList()

init {
require(cards.toSet().size == cards.size) { ERROR_EXIST_DUPLICATE_CARDS }
require(cards.size == CARDS_SIZE) { ERROR_INVALID_CARDS_SIZE }
}

val size: Int
get() = cards.size

Choose a reason for hiding this comment

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

코틀린 컨벤션에 따라 위치를 조정하면 좋을 것 같아요.

  1. Property declarations and initializer blocks
  2. Secondary constructors
  3. Method declarations
  4. Companion object


fun nextCard(): Card = cards.removeFirst()
companion object {
private const val CARDS_SIZE = 52
private const val ERROR_INVALID_CARDS_SIZE = "카드덱 초기 사이즈는 ${CARDS_SIZE}장이어야 합니다."
private const val ERROR_EXIST_DUPLICATE_CARDS = "카드덱에는 중복이 없어야 합니다."
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/CardMark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain

enum class CardMark {
CLOVER,
HEART,
SPADE,
DIA,
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/CardValue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain

enum class CardValue(val value: Int) {
ACE(1),
KING(10),
QUEEN(10),
JACK(10),
TEN(10),
NINE(9),
EIGHT(8),
SEVEN(7),
SIX(6),
FIVE(5),
FOUR(4),
THREE(3),
TWO(2),
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/Cards.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

class Cards(private val cards: Set<Card> = setOf()) {
val size: Int
get() = cards.size

fun toList() = cards.toList()

operator fun plus(card: Card): Cards = Cards(cards.plus(card))

fun containsACE() = cards.map { it.value }.contains(CardValue.ACE)

Choose a reason for hiding this comment

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

isContainsAce()라는 네이밍이 좀 더 어울리지 않을까요?


companion object {
private val CARDS: List<Card> = CardMark.values().flatMap { mark ->
CardValue.values().map { value -> Card(mark, value) }
}

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

class Dealer(name: String = "딜러") : User(name) {
override val isContinue: Boolean
get() = Score(cards).maxScore < DEALER_MIN_NUMBER

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

class Guest(name: String) : User(name) {
override val isContinue: Boolean
get() = isNotBust && isBlackJack.not()

private val isNotBust: Boolean
get() = Score(cards).minScore <= BLACKJACK_NUMBER

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

@JvmInline
value class Name(private val value: String) {

Choose a reason for hiding this comment

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

value class 사용 굿 👍

init {
require(value.length in NAME_LENGTH_MIN..NAME_LENGTH_MAX) { ERROR_NAME_LENGTH_MAX }
}

override fun toString(): String = value

companion object {
private const val NAME_LENGTH_MIN = 1
private const val NAME_LENGTH_MAX = 20
private const val ERROR_NAME_LENGTH_MAX = "이름의 글자수는 $NAME_LENGTH_MIN~${NAME_LENGTH_MAX}사이 입니다."
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/blackjack/domain/Outcome.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package blackjack.domain
import java.lang.IllegalStateException

enum class Outcome {
WIN, DRAW, LOSE;

companion object {
private const val BLACKJACK_NUMBER = 21

fun User.winTo(other: User): Outcome =
when {
other.score > BLACKJACK_NUMBER && this.score > BLACKJACK_NUMBER -> DRAW
other.score > BLACKJACK_NUMBER -> WIN
this.score > BLACKJACK_NUMBER -> LOSE

other.score == this.score -> DRAW
this.score > other.score -> WIN
other.score > this.score -> LOSE

else -> throw IllegalStateException()
}
}

Choose a reason for hiding this comment

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

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

data class Participants(
val dealer: Dealer,
val guests: List<Guest>,
) {
fun all(): List<User> = listOf(dealer) + guests
}
12 changes: 12 additions & 0 deletions src/main/kotlin/blackjack/domain/ParticipantsBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package blackjack.domain

class ParticipantsBuilder {
private lateinit var dealer: Dealer
private lateinit var guests: List<Guest>

fun dealer() { dealer = Dealer() }

fun guests(names: List<String>) { guests = names.map { Guest(it) } }

fun build(): Participants = Participants(dealer, guests)
}
23 changes: 23 additions & 0 deletions src/main/kotlin/blackjack/domain/Score.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package blackjack.domain

class Score(val cards: Cards) {

Choose a reason for hiding this comment

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

3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

위 요구 사항을 지켜보도록 도전해보세요.

Choose a reason for hiding this comment

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

Score에 대한 테스트 코드도 작성해보시죠!

val score: Int
get() = if (isMaxScoreInRange) maxScore else minScore

val minScore: Int
get() = cards.toList().sumOf { it.value.value }

val maxScore: Int
get() = minScore + if (cards.containsACE() && validateAceCondition) ACE_OTHER_NUMBER_DIFF else 0

private val validateAceCondition: Boolean
get() = minScore + ACE_OTHER_NUMBER_DIFF <= BLACKJACK_NUMBER

private val isMaxScoreInRange: Boolean
get() = maxScore <= BLACKJACK_NUMBER

Choose a reason for hiding this comment

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

보통 Boolean 타입은 is로 시작하죠. 네이밍에 일관성을 맞추면 더 좋겠어요.


companion object {
private const val ACE_OTHER_NUMBER_DIFF = 10
private const val BLACKJACK_NUMBER = 21

Choose a reason for hiding this comment

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

BLACKJACK_NUMBER = 21
같은 상수가 여러 곳에서 사용중입니다. 만약 해당 값에 변경이 발생한다면 모두 찾아서 변경해줘야 하며 실수로 빠뜨리면 버그가 발생할 수 있습니다. 하나의 상수로 해결 할 수 있는 구조에 대해 고민해보면 어떨까요?

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

abstract class User(name: String) {
val name = Name(name)
var cards = Cards()
val score: Int
get() = Score(cards).score

val isBlackJack: Boolean
get() = Score(cards).score == BLACKJACK_NUMBER

abstract val isContinue: Boolean

fun draw(card: Card) { cards += card }

companion object {
private const val BLACKJACK_NUMBER = 21
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package blackjack.view

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

Choose a reason for hiding this comment

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

모든 원시 값과 문자열을 포장한다

이름을 공백으로 여러명을 넣어도 동작을 하네요.
원시 값 포장을 통해서 예외 처리를 하면 어떨까요?

}

fun inputDrawMore(name: String): String {
println("\n${name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)")
return readln()
}
}
Loading