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

2단계 - 블랙잭 #682

Open
wants to merge 10 commits into
base: hyotaek-jang
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions src/main/kotlin/blackjack/BlackJackController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package blackjack

import blackjack.BlackJackController.initPlayer
import blackjack.BlackJackController.initSetting
import blackjack.BlackJackController.progressGame
import blackjack.domain.Deck
import blackjack.domain.Player
import blackjack.view.InputView
import blackjack.view.OutputView

object BlackJackController {
fun initPlayer(names: List<String>): List<Player> {
return names.map { Player(it) }
}

fun initSetting(players: List<Player>, deck: Deck) {
players.forEach {
it.addCard(deck.draw(2))
}
}

fun progressGame(player: Player, deck: Deck) {
while (player.canDraw() && InputView.inputHitOrStand(player.name)) {
player.addCard(deck.draw())
OutputView.printCards(player.name, player.cards)
}
}
}

fun main() {
val deck = Deck.create()
val players = initPlayer(InputView.inputNames())

initSetting(players, deck)
OutputView.printDrawTwoCards(players)

players.forEach { progressGame(it, deck) }
OutputView.printPlayersScore(players)
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# kotlin-blackjack

## 기능 요구사항

- [x] 카드의 종류는 스페이드, 다이아몬드, 하트, 클로버의 4가지로 한다.
- [x] 숫자는 각각 1에서 10까지의 점수를 갖는다.
- [x] A는 1 또는 11로 계산할 수 있다.
- [x] A를 11로 계산했을 때 21을 초과하면 1로 계산한다.
- [x] J, Q, K는 각각 10으로 계산한다.
- [x] 게임에 참여할 사람의 이름은 쉼표(,)를 기준으로 구분한다.
- [x] 게임을 시작하면 각 플레이어는 각자 두 장의 카드를 지급 받는다.
- [x] 받은 카드를 출력한다.
- [x] 카드 숫자를 모두 합쳐 21을 초과하지 않으면 카드를 더 받을 수 있다.
- [x] 한 사용자의 받기가 끝나면 다음 사용자로 넘어간다.
- [x] 예는 y, 아니오는 n을 입력한다.
- [x] 카드를 받을 때마다 현재 갖고 있는 카드를 출력한다.
- [x] 더이상 카드를 받지 않는다면 각 플레이어는 각자가 가진 카드의 합을 계산한다.
14 changes: 14 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package blackjack.domain

object BlackJackGame {
const val MAX_SCORE = 21
private const val ADDITIONAL_ACE_SCORE = 10

fun score(cards: CardList): Int {
Copy link

Choose a reason for hiding this comment

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

코멘트 남겨주신것 처럼 BlackJackGame의 책임이 모호한거 같아요 😄
플레이어가 뽑은 카드의 점수를 계산하는 책임을 부여 해주셨는데 개인적으로는 CardList 일급 컬렉션이 해당 기능을 제공하는것이 자연스러운 모델링이 될수 있을것 같은데 검토 부탁드려요 😄

var score = cards.cards.sumOf { it.score() }
if (cards.hasAce() && score + ADDITIONAL_ACE_SCORE <= MAX_SCORE) {
score += ADDITIONAL_ACE_SCORE
}
return score
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package blackjack.domain

class Card(private val suit: Suit, private val rank: Rank) {
fun score(): Int {
return rank.score
}

fun isAce(): Boolean {
return rank == Rank.ACE
}

override fun toString(): String {
return "${rank.outputName}${suit.value}"
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/domain/CardList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blackjack.domain

class CardList(private val _cards: MutableList<Card> = mutableListOf()) {
val cards: List<Card>
get() = _cards

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

fun addCard(newCards: List<Card>) {
_cards.addAll(newCards)
}

fun hasAce(): Boolean {
return _cards.any(Card::isAce)
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/blackjack/domain/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package blackjack.domain

import java.util.Stack

class Deck(private val cards: Stack<Card>) {
fun draw(): Card {
require(cards.isNotEmpty()) { "카드가 없습니다." }
return cards.pop()
}

fun draw(count: Int): List<Card> {
require(cards.size >= count) { "카드가 없습니다." }
return (1..count).map { cards.pop() }
}

companion object {
fun create(): Deck {
val cards: List<Card> = Rank.getRankSet().values
.flatMap { rank -> Suit.getSuitSet().values.map { suit -> Card(suit, rank) } }
.shuffled()

return Deck(
Stack<Card>().apply {
addAll(cards)
}
)
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/domain/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blackjack.domain

import blackjack.domain.BlackJackGame.MAX_SCORE
import blackjack.domain.BlackJackGame.score

class Player(val name: String, val cards: CardList = CardList()) {
Copy link

Choose a reason for hiding this comment

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

CardList라는 네이밍도 좋지만 플레이어가 뽑은 카드를 직관적으로 나타낼수 있는 네이밍을 고민해보는건 어떨까요 ?

fun addCard(newCard: Card) {
Copy link

Choose a reason for hiding this comment

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

참가자가 카드를 추가하는것도 좋지만 카드를 뽑는 행위를 제공하는것도 좋을것 같아요 😄

cards.addCard(newCard)
}

fun addCard(newCards: List<Card>) {
cards.addCard(newCards)
}
Comment on lines +11 to +13
Copy link

Choose a reason for hiding this comment

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

플레이어가 카드를 복수로 뽑는 행위는 현재 처음 카드를 받는 상황만 존재할것 같아요. addCard 보단 블랙잭 게임을 시작하는것을 나타내보는건 어떨까요 ?


fun canDraw(): Boolean {
Copy link

Choose a reason for hiding this comment

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

플레이어가 카드를 뽑지 못하는 상황은 뽑은 카드의 합이 21이 넘었을 경우와 스스로 카드를 그만 받는다고 선언한 상태가 존재합니다.
외부 요인(input value)에 의존하지 않고 스스로 상태를 나타내보는건 어떨까요 ?

return score(cards) < MAX_SCORE
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/blackjack/domain/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blackjack.domain

enum class Rank(val outputName: String, val score: Int) {
ACE("A", 1),
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),
J("J", 10),
Q("Q", 10),
K("K", 10);

companion object {
private val RANK_SET = values().associateBy { it.outputName }

fun getRankSet(): Map<String, Rank> {
return RANK_SET
}
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/blackjack/domain/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package blackjack.domain

enum class Suit(val value: String) {
SPADE("스페이드"), DIAMOND("다이아몬드"), HEART("하트"), CLUB("클로버");

companion object {
private val SUIT_SET = values().associateBy { it.value }

fun getSuitSet(): Map<String, Suit> {
return SUIT_SET
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.view

object InputView {
private const val DELIMITER = ","
private val RESPONSE_MAP = mapOf("y" to true, "n" to false)

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

fun inputHitOrStand(name: String): Boolean {
println("\n${name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)")
return RESPONSE_MAP[readln()] ?: throw IllegalArgumentException("잘못된 입력입니다.")
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/blackjack/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package blackjack.view

import blackjack.domain.BlackJackGame
import blackjack.domain.CardList
import blackjack.domain.Player

object OutputView {
private const val DELIMITER = ", "

fun printCards(name: String, cards: CardList) {
println("${name}카드: ${cards.cards.joinToString(DELIMITER)}")
}

fun printDrawTwoCards(players: List<Player>) {
println("\n${players.joinToString(DELIMITER) { it.name }}에게 2장의 카드를 나누었습니다.")
players.forEach { printCards(it.name, it.cards) }
}

fun printPlayersScore(players: List<Player>) {
println()
players.forEach { printPlayerScore(it, it.cards) }
}

private fun printPlayerScore(player: Player, cards: CardList) {
println("${player.name}카드 : ${cards.cards.joinToString(DELIMITER)} - 결과: ${BlackJackGame.score(cards)}")
}
}
1 change: 0 additions & 1 deletion src/main/kotlin/dsl/domain/PersonBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,3 @@ class PersonBuilder {
return Person(name, company, skills, languages)
}
}

3 changes: 1 addition & 2 deletions src/main/kotlin/dsl/domain/Skills.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
package dsl.domain

class Skills(val soft: List<String>, val hard: List<String>) {
}
class Skills(val soft: List<String>, val hard: List<String>)
42 changes: 42 additions & 0 deletions src/test/kotlin/blackjack/domain/BlackJackGameTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package blackjack.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class BlackJackGameTest {
@Test
fun `카드 점수 계산 - 에이스를 제외하고 카드 점수의 총합이 11점 이상이면 에이스는 1점이 된다`() {
// given
val cardList = CardList()
val cards = listOf(
Card(Suit.SPADE, Rank.ACE),
Card(Suit.DIAMOND, Rank.NINE),
Card(Suit.HEART, Rank.TWO)
)
cardList.addCard(cards)

// when
val score = BlackJackGame.score(cardList)

// then
assertThat(score).isEqualTo(12)
}

@Test
fun `카드 점수 계산 - 에이스를 제외하고 카드 점수의 총합이 10점 이하면 에이스는 11점이 된다`() {
// given
val cardList = CardList()
val cards = listOf(
Card(Suit.SPADE, Rank.ACE),
Card(Suit.DIAMOND, Rank.SIX),
Card(Suit.HEART, Rank.FOUR)
)
cardList.addCard(cards)

// when
val score = BlackJackGame.score(cardList)

// then
assertThat(score).isEqualTo(21)
}
}
36 changes: 36 additions & 0 deletions src/test/kotlin/blackjack/domain/CardListTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package blackjack.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class CardListTest {
@Test
fun `단건 카드 추가`() {
// given
val cardList = CardList()
val cards = listOf(Card(Suit.SPADE, Rank.ACE))

// when
cardList.addCard(cards)

// then
assertThat(cardList.cards).containsAll(cards)
}

@Test
fun `여러 카드 추가`() {
// given
val cardList = CardList()
val cards = listOf(
Card(Suit.SPADE, Rank.ACE),
Card(Suit.DIAMOND, Rank.NINE),
Card(Suit.HEART, Rank.TWO)
)

// when
cardList.addCard(cards)

// then
assertThat(cardList.cards).containsAll(cards)
}
}
28 changes: 28 additions & 0 deletions src/test/kotlin/blackjack/domain/DeckTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackjack.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException

internal class DeckTest {
@Test
fun `Deck은 52장의 카드를 갖는다 따라서 그 이상의 카드를 draw하면 오류가 발생한다`() {
// given, when
val deck = Deck.create()
val deckSize = 52

// then
assertThrows<IllegalArgumentException> { deck.draw(deckSize + 1) }
}

@Test
fun `Deck은 52장의 카드를 갖는다`() {
// given, when
val deck = Deck.create()
val deckSize = 52

// then
assertThat(deck.draw(deckSize).size).isEqualTo(deckSize)
}
}
25 changes: 25 additions & 0 deletions src/test/kotlin/blackjack/domain/PlayerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blackjack.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class PlayerTest {
@Test
fun `21점을 넘지 않으면 플레이어는 게임을 계속할 수 있다`() {
// given
val player = Player("test")
val cardList = CardList()
val cards = listOf(
Card(Suit.SPADE, Rank.ACE),
Card(Suit.DIAMOND, Rank.NINE),
Card(Suit.HEART, Rank.TWO)
)
cardList.addCard(cards)

// when
val isContinue = player.canDraw()

// then
assertThat(isContinue).isTrue()
}
}
Loading