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

Step2 #645

Open
wants to merge 11 commits into
base: kakao-moses-lee
Choose a base branch
from
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# kotlin-blackjack
# kotlin-blackjack

## step2 - 블랙잭

기능 요구사항
- 딜러
- [x] : 게임 시작 시에, Player 에게 카드를 2장씩 나눠준다.
- [x] : Player 에게 카드를 나눠준다.
- [x] : Player 에게 카드를 나눠줄 수 있는지 확인한다.
- 플레이어
- [x] : 딜러에게 카드를 달라고 한다.
- [x] : 받은 카드의 총합을 계산한다.
- 게임
- [x] : N 명의 플레이어가 play 할 수 있다.

## 용어 정리
- Hand : 플레이어가 들고 있는 패
- Card : 카드 (단수)
- Rank : 무늬별로 A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K의 13 가지 끗수(rank)
- Suit : 스페이드, 하트, 다이아몬드, 클럽의 4 가지 무늬(suit)
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ repositories {
dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2")
testImplementation("org.assertj", "assertj-core", "3.22.0")
testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3")
testImplementation("io.kotest", "kotest-runner-junit5", "5.7.2")
testImplementation("io.kotest", "kotest-assertions-core-jvm", "5.7.2")
}

tasks {
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/blackJack/BlackJackRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackJack

import blackJack.controller.BlackJackController

class BlackJackRunner

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

import blackJack.model.Dealer
import blackJack.model.Player
import blackJack.model.askMoreCard
import blackJack.model.checkDrawCardIsAllowedFor
import blackJack.view.InputView
import blackJack.view.OutputView

class BlackJackController {
fun play() {
val req = InputView.getNames()

Choose a reason for hiding this comment

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

req 라는 이름 대신 명확하게 변수 이름을 정해주면 좋을 것 같아요!
이하 동일합니다.


val candidates = req.map { Player(it) }
val dealer = Dealer("dealer")

val players = dealer.startGame(candidates)
OutputView.printPlayersState(players)

players.forEach { player ->
shouldContinue(player, dealer)
}

OutputView.printFinalState(players)
}

private fun shouldContinue(player: Player, dealer: Dealer) {
while (true) {
val req = InputView.getPlayerInput(player.name)
if (req == "n") {
break
}
player askMoreCard dealer

if ((dealer checkDrawCardIsAllowedFor player).not()) {
break
}
OutputView.printPlayerState(player)
}
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/blackJack/model/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackJack.model

import blackJack.model.enums.Rank
import blackJack.model.enums.Suit

data class Card(
val suit: Suit,
val rank: Rank
)
25 changes: 25 additions & 0 deletions src/main/kotlin/blackJack/model/CardDeck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blackJack.model

import blackJack.model.enums.Rank
import blackJack.model.enums.Suit

class CardDeck(val cards: List<Card>) {
companion object {
fun of(): CardDeck {
val cards = generateAllCards()
return CardDeck(cards)
}
Comment on lines +8 to +11

Choose a reason for hiding this comment

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

플레잉 카드는 작성해주신 대로 생성할 때 중복되지 않은 13개의 끗수와 4개의 모양, 총 52개의 카드를 가지고 있습니다.
그렇다면 이러한 기능(도메인)을 잘 지키고 있는지 테스트를 작성 해 볼수 있지 않을까요?


private fun generateAllCards(): List<Card> {
return Suit.values().flatMap { suit ->
generateCardsForSuit(suit)
}
}

private fun generateCardsForSuit(suit: Suit): List<Card> {
return Rank.values().map { rank ->
Card(suit, rank)
}
}
}
}
39 changes: 39 additions & 0 deletions src/main/kotlin/blackJack/model/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package blackJack.model

class Dealer(val name: String) {
private var cardDeck = CardDeck.of()
Comment on lines +3 to +4
Copy link
Author

Choose a reason for hiding this comment

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

카지노에 Dealer 마다 CarDeck 이 여러개 인것처럼,

Dealer 에게 할당되는 CarDeck 이 있다고 생각했습니다.

Choose a reason for hiding this comment

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

딜러가 처음 가지고 있는 덱을 생성자로 받도록 하면 테스트에 조금 더 유연하게 작성할 수 있지 않을까요?

val MAXIMUM_SCORE = 21

fun countCard(): Int {
return cardDeck.cards.size
}

fun drawCard(): Card {
val currentCard = cardDeck.cards
.shuffled()

Choose a reason for hiding this comment

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

카지노라고 가정하더라도 카드를 뽑을 때마다 매번 섞지는 않지 않을까요!??

.first()

cardDeck = cardDeck.cards
.filter { it != currentCard }
.let(::CardDeck)
Comment on lines +16 to +18

Choose a reason for hiding this comment

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

CarDeck 의 cards 를 매번 새로 생성해서, 불변 객체로 관리하고자 했습니다.

불변 객체 관리 좋습니다. 👍🏻
다만 카드를 한장 뽑아 상태를 변경하는 것은 CardDeck이 스스로 하도록 만들어 보면 어떨까요?

cardDeck = cardDeck.drawFirst()


return currentCard
}
Comment on lines +11 to +21
Copy link
Author

Choose a reason for hiding this comment

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

CarDeck 의 cards 를 매번 새로 생성해서, 불변 객체로 관리하고자 했습니다.


fun startGame(players: List<Player>): List<Player> {
return initializePlayerHands(players)
}

private fun initializePlayerHands(players: List<Player>): List<Player> {
players.forEach { player ->
player requestCardToDealer drawCard()
player requestCardToDealer drawCard()
}

return players
}
Comment on lines +23 to +34

Choose a reason for hiding this comment

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

카지노에 Dealer 마다 CarDeck 이 여러개 인것처럼,
Dealer 에게 할당되는 CarDeck 이 있다고 생각했습니다.

카지노라고 가정했기 때문에 딜러가 모든 게임을 진행하는 주체가 되었네요.
그렇다면 이 딜러는 여러개의 게임을 한번에 진행할 수 있어 보이는데요.

val dealer: Dealer
val players1: List<Player>
val players2: List<Player>
...

dealer.startGame(players1)
dealer.startGame(players2)
dealer.startGame(players1)
...

제가 생각한 바로는 딜러도 결국 하나의 블랙잭 게임에 참여하는 구성원으로 볼 수 있지는 않을까요?
게임의 진행 과정과 상태를 판단하고 처리하는 역할을 어떤 객체가 담당해야 할지 고민이 들었습니다.

class BlackJackGame(딜러, 플레이어들, 덱)

}

infix fun Dealer.checkDrawCardIsAllowedFor(player: Player): Boolean {
return player.calculateScore() < MAXIMUM_SCORE
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackJack/model/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackJack.model

class Player(
val name: String,
var hand: List<Card> = listOf()

Choose a reason for hiding this comment

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

중위함수를 객체 외부에 작성하면서 외부에서 값을 수정할 수 있도록 var 가 되었어요.
이는 어디서든 이 객체의 hand 값을 바꿀 수 있다는 위험성을 가지게 됩니다.

불변 객체를 유지하는 것을 목표로 한다면, 어떠한 동작이 새로운 Player를 반환하도록 만들 수 있지 않을까요?
그렇지 않다면 backing property 등을 이용해 내부에서 가변 객체를 가지는 전략을 사용해 보면 좋을 것 같습니다.

) {
fun calculateScore(): Int {
return hand.sumOf { it.rank.score }
}
}

infix fun Player.askMoreCard(dealer: Dealer) {
if (dealer checkDrawCardIsAllowedFor this) {
this requestCardToDealer dealer.drawCard()
}
}

infix fun Player.requestCardToDealer(card: Card) {
hand += card
}
Comment on lines +12 to +20
Copy link
Author

Choose a reason for hiding this comment

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

가독성을 위해 중위 함수를 선언해서 사용했습니다.

프로젝트 규모가 커졌을 경우, 이러한 함수들 관리가 어려울 수도 있을거 같은데요.

유지보수 측면에서 확장함수를 활용한 중위 함수 사용이 적절한지 조언 부탁드립니다!

Choose a reason for hiding this comment

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

중위함수도 결국 함수이기 때문에 프로젝트의 규모와 관계없다고 생각을 합니다.
다만 작성하신 코드를 통해 드는 의문은 아래와 같습니다.

  1. 유지보수르 고려한다면 왜 비즈니스 로직이 객체 내부에 있지 않고 외부에 작성되었을까?
  2. 중위 함수일 필요가 있는 형태인가?

코틀린에서 사용되는 대표적인 중위 함수로는 in operator 가 있는데요.
누가봐도 명확하게 이해가 가는 형태가 되어야 합니다.

val contains = player in players

21 changes: 21 additions & 0 deletions src/main/kotlin/blackJack/model/enums/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackJack.model.enums

Choose a reason for hiding this comment

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

enums 를 별도의 패키지로 관리하는 것은 어떤 이점이 있나요?
sealed class, interface, abstract class도 모두 패키지가 나뉘어야 한다고 봐야 할까요?

만약 더이상 enum class가 아니게 된다면 패키지도 바꿔야 하는 문제가 되지는 않을까요?


enum class Rank(
val symbol: String,
val score: Int,
val isAce: Boolean = false
) {
ACE("A", 1, true),
TWO("2", 2),
THREE("3", 3),
FOUR("4", 4),
FIVE("5", 5),
SIX("6", 6),
SEVEN("7", 7),
EIGHT("8", 8),
NINE("9", 9),
TEN("10", 10),
JACK("J", 10),
QUEEN("Q", 10),
KING("K", 10),
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackJack/model/enums/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackJack.model.enums

enum class Suit(val symbol: String) {
CLUBS("클로버"),
DIAMONDS("다이아몬드"),
HEARTS("하트"),
SPADES("스페이드")
}
Comment on lines +3 to +8

Choose a reason for hiding this comment

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

symbol의 경우 어떻게 출력할지는 UI의 관심사로 볼 수 있지 않을까요?
가령 스페이드를 "♤" 이렇게 출력하게 된다면 도메인 모델의 수정이 불가피합니다.

22 changes: 22 additions & 0 deletions src/main/kotlin/blackJack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackJack.view

object InputView {
private const val PLAYER_QUERY_FORMAT = "%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"

fun getNames(): List<String> {
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)")
val input = readlnOrNull() ?: throw IllegalArgumentException("콘솔 입력을 확인해 주세요.")

return input.replace(" ", "")
.split(",")
Comment on lines +10 to +11

Choose a reason for hiding this comment

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

순서의 차이같지만, trim() 을 사용하도록 만들 수도 있을 것 같네요.

input.split(",")
   .map { it.trim() }

}

fun getPlayerInput(playerName: String): String {

Choose a reason for hiding this comment

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

getPlayerInput()이 반환하는 String이 무엇인지 예측하기 어려워 보입니다.
명확하게 Y 혹은 N을 반환해야 한다면 이것도 enum class로 만들어 볼 수 있지 않을까요?

println(PLAYER_QUERY_FORMAT.format(playerName))

val input = readlnOrNull() ?: throw IllegalArgumentException("콘솔 입력을 확인해 주세요.")
require(input == "y" || input == "n") ?: throw IllegalArgumentException("y 또는 n을 입력해 주세요.")

return input
}
}

Choose a reason for hiding this comment

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

gradlew ktlintCheck 를 확인 해 주세요!

blackJack\view\InputView.kt:1:1 File must end with a newline (\n) (final-newline)

28 changes: 28 additions & 0 deletions src/main/kotlin/blackJack/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackJack.view

import blackJack.model.Player

object OutputView {
private const val PLAYER_STATE_FORMAT = "%s카드 : %s"
private const val PLAYER__FINAL_STATE_FORMAT = "%s카드 : %s - 결과: %s"

fun printPlayersState(players: List<Player>) {
players.forEach { printPlayerState(it) }
}

fun printFinalState(players: List<Player>) {
players.forEach { printPlayerFinalState(it) }
}

fun printPlayerState(player: Player) {
player.hand
.joinToString { it.rank.symbol + it.suit.symbol }
.let { println(PLAYER_STATE_FORMAT.format(player.name, it)) }
}

private fun printPlayerFinalState(player: Player) {
player.hand
.joinToString { it.rank.symbol + it.suit.symbol }
.let { println(PLAYER__FINAL_STATE_FORMAT.format(player.name, it, player.calculateScore())) }
}
Comment on lines +9 to +27
Copy link
Author

Choose a reason for hiding this comment

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

MVC 패턴의 정의에 따라서 View 를 작성했습니다.

  • view 는 model 에만 의존해야하고 (= view 내부에 model 의 코드만 있을 수 있고)

Q. view 가 model 을 알게됨으로 발생하는 비용은 없나요?
= view 가 아예 model 을 모르도록 dto 를 사용하면 이점이 있을까요?

Choose a reason for hiding this comment

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

view 가 model 을 알게됨으로 발생하는 비용은 없나요?

질문에서 이미 답을 어느정도 가지고 계신 것 같습니다.
Model이 가변 객체라면 View에서 의도치 않은 상태 변경을 야기할 수는 있겠습니다.
이는 흔히 DTO 를 이용한 MVC 패턴에서의 문제점과 같구요.

DTO를 이용해서 출력을 하는 것 자체는 관심사 분리라는 이점을 가지지만 충분히 이해하고 있다면 미션에서 이를 적용하시지는 않아도 괜찮습니다.

}
2 changes: 1 addition & 1 deletion src/main/kotlin/study/builder/LangaugeBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package study.builder

import study.dto.Language
import study.domain.Language

class LangaugeBuilder {
private var languages: List<Language> = emptyList()
Expand Down
14 changes: 7 additions & 7 deletions src/main/kotlin/study/builder/PersonBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package study.builder

import study.dto.Language
import study.dto.Person
import study.dto.Skill
import study.domain.Language
import study.domain.Person
import study.domain.Skill

class PersonBuilder {
private lateinit var name: String
private lateinit var company: String
private lateinit var skills: List<Skill>
private lateinit var languages: List<Language>
private var name: String = "홍길동"
private var company: String = "미정"
private var skills: List<Skill> = emptyList()
private var languages: List<Language> = emptyList()

fun name(value: String) {
name = value
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/study/builder/SkillsBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package study.builder

import study.dto.Skill
import study.domain.Skill

class SkillsBuilder {
private var skills: List<Skill> = emptyList()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package study.dto
package study.domain

data class Language(val name: String, val level: Int)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package study.dto
package study.domain

data class Person(val name: String, val company: String, val skills: List<Skill>, val languages: List<Language>)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package study.dto
package study.domain

data class Skill(val description: String)
6 changes: 3 additions & 3 deletions src/main/kotlin/study/dsl/Person.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package study.dsl
import study.builder.LangaugeBuilder
import study.builder.PersonBuilder
import study.builder.SkillsBuilder
import study.dto.Language
import study.dto.Person
import study.dto.Skill
import study.domain.Language
import study.domain.Person
import study.domain.Skill

fun introduce(block: PersonBuilder.() -> Unit): Person {
return PersonBuilder().apply(block).build()
Expand Down
48 changes: 48 additions & 0 deletions src/test/kotlin/blackjack/domain/DealerSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package blackjack.domain

import blackJack.model.Dealer
import blackJack.model.Player
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe

class DealerSpec : BehaviorSpec({
given("딜러와 플레이어 2명이 있을떄") {
val dealer = Dealer("dealer")
val player1 = Player("player1")
val player2 = Player("player2")
val players = listOf(player1, player2)

`when`("게임을 시작했을때") {
dealer.startGame(players)

then("플레이어는 딜러가 나눠준 카드를 두장씩 갖고 있다.") {
players[0].hand.size shouldBe 2
players[1].hand.size shouldBe 2
}
}
}

given("딜러에게 카드덱이 주어지고") {
val dealer = Dealer("dealer")

`when`("딜러가 게임을 시작했을때") {
val players = listOf(Player("player1"), Player("player2"))
dealer.startGame(players)

then("플레이어는 딜러가 나눠준 카드를 두장씩 갖고 있다.") {
players[0].hand.size shouldBe 2
players[1].hand.size shouldBe 2
}
}

`when`("딜러에게 카드를 한장 나눠줬을때") {
val prevCount = dealer.countCard()
dealer.drawCard()
val currentCount = dealer.countCard()

then("카드덱에는 한장의 카드가 사라졌다.") {
currentCount shouldBe prevCount - 1
}
}
Comment on lines +38 to +46

Choose a reason for hiding this comment

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

이 부분에 있어서도 카드덱을 유연하게 구성한다면 조금 더 간단하게 만들 수 있어 보이네요!

2장의 카드를 가지고 있을 때 1장을 나눠주면 보유한 카드는 총 1장이다.

}
})
Loading