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: 블랙잭 #567

Open
wants to merge 22 commits into
base: ggam-nyang
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9cc02c0
docs: README 기능 목록 작성
ggam-nyang Jul 3, 2023
c776ef4
feature: 카드는 숫자와 문양을 가진다
ggam-nyang Jul 3, 2023
d4bf8d5
feature: 카드 숫자는 Ace ~ King까지 있다.
ggam-nyang Jul 3, 2023
b298e63
feature: J Q K는 값이 10이다
ggam-nyang Jul 3, 2023
6b736c6
feature: 카드 문양은 CardSuit enum으로 나타낸다
ggam-nyang Jul 5, 2023
a62a5fe
feature: CardDeck은 총 개수가 52장이다
ggam-nyang Jul 5, 2023
ca375ee
feature: Card는 동일한 suit, rank에 대해 같은 인스턴스를 반환한다.
ggam-nyang Jul 5, 2023
d3def5f
feature: 딜러는 CardDeck을 가지고 있다
ggam-nyang Jul 5, 2023
d083387
feature: 딜러는 카드를 뽑을 수 있고, 그 카드는 덱에서 사라진다
ggam-nyang Jul 5, 2023
725dc3a
feature: Player 생성 및 addAll 메서드 생성
ggam-nyang Jul 5, 2023
a182eda
feature: 딜러의 deal 메서드 추가
ggam-nyang Jul 5, 2023
4c92683
feature: 플레이어는 게임 시작 시 카드 두 장을 지급받는다.
ggam-nyang Jul 5, 2023
30dfebd
feature: 플레이어는 이름을 가지고 게임 시작 시 플레이어의 이름을 입력 할 수 있다.
ggam-nyang Jul 5, 2023
dc83d17
feature: 게임 시작 시 플레이어의 카드를 출력한다.
ggam-nyang Jul 6, 2023
6b64a20
feature: 플레이어는 손패의 합을 알고, draw 가능한지 여부를 안다
ggam-nyang Jul 10, 2023
99c6ddb
feature: 플레이어는 카드 뽑기를 멈출 수 있고, State를 가진다
ggam-nyang Jul 10, 2023
5389302
refactor: 도메인 하위 패키지 생성
ggam-nyang Jul 10, 2023
5339816
refactor: 카드를 뽑을 때마다 현재 손패를 출력한다.
ggam-nyang Jul 10, 2023
7056b8d
refactor: blackjackGame 게임 시작 로직을 start 메서드로 이동
ggam-nyang Jul 14, 2023
715b3d4
feature: Ace는 1 또는 10으로 계산된다
ggam-nyang Jul 14, 2023
fd9a249
feature: 결과 출력 추가
ggam-nyang Jul 14, 2023
09664a4
refactor: 패키지 구조 변경
ggam-nyang Aug 1, 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
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/Client.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack

import blackjack.domain.BlackjackGame
import blackjack.domain.player.Dealer
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val dealer = Dealer()
val players = InputView.inputPlayers()
val blackJackGame = BlackjackGame(dealer, players)

OutputView.printInitGame(blackJackGame)
blackJackGame.start()
OutputView.printResult(blackJackGame.players)
}
30 changes: 30 additions & 0 deletions src/main/kotlin/blackjack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Step2

### 기능 목록
- [x] 카드는 숫자와 문양을 가진다.
- [x] 카드의 숫자는 1(Ace)부터 King까지 있다.
- [x] King, Queen, Jack은 각 10으로 계산된다.
- [x] 문양은 하트, 다이아몬드, 클로버, 스페이드 4가지이다.
- [x] 카드의 총 개수는 13 * 4인 52장이다.
- [x] 카드는 문양, 숫자가 동일하다면 모두 같은 인스턴스를 반환한다.
- [x] 딜러는 카드 뭉치를 가지고 있다.
- [x] 딜러는 랜덤한 카드를 뽑을 수 있고, 뽑은 카드는 덱에서 사라진다.
- [x] 플레이어는 여러 장의 카드를 가지고 있다.
- [x] 딜러는 플레이어에게 카드를 줄 수 있다.
- [x] 플레이어는 게임 시작 시 카드 두 장을 지급받는다.
- [x] 플레이어는 카드를 뽑을 수 있다.
- [x] 게임 시작 시 플레이어의 이름을 입력할 수 있다.
- [x] 게임 시작 시 플레이어의 카드를 출력한다.
- [x] 게임 시작 후, 각 플레이어는 카드를 뽑을 지 선택할 수 있다.
- [x] 플레이어는 카드를 뽑기를 멈출 수 있다.
- [x] 플레이어는 숫자 합이 21이 넘으면 카드를 뽑을 수 없다.
- [x] Ace는 숫자 합에 따라 1 또는 11로 계산할 수 있다.
- [x] 각 플레이어의 결과를 출력할 수 있다.

### 기능 요구사항
- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며
- King, Queen, Jack은 각각 10으로 계산한다.
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받는다.
- 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다.
- 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.

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

import blackjack.domain.player.Dealer
import blackjack.domain.player.Player
import blackjack.domain.state.PlayerState
import blackjack.view.InputView
import blackjack.view.OutputView

class BlackjackGame(
private val dealer: Dealer,
val players: List<Player>
) {
init {
initGame()
}

private fun initGame() {
repeat(INIT_DEAL_COUNT) { initDeal() }
}

private fun initDeal() {
players.forEach { player -> dealer.deal(player) }
}

fun isNotFinished(): Boolean = players.any { it.state == PlayerState.PLAYING }

fun start() {
while (isNotFinished()) {
players.forEach { player ->
if (player.state == PlayerState.PLAYING) {
val inputDrawResponse = InputView.inputDrawResponse(player)
if (inputDrawResponse) {
dealer.deal(player)
OutputView.printCardsInHandWithEmptyLine(player)
} else player.stopDraw()
}
}
}
}

companion object {
private const val INIT_DEAL_COUNT = 2
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/blackjack/domain/card/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package blackjack.domain.card

class Card private constructor (
val suit: CardSuit,
val rank: CardRank
) {
fun print(): String = rank.forOutput + suit.forOutput

companion object {
private const val INVALID_CARD_ERROR = "존재하지 않는 카드입니다."
val allCards =
CardSuit.values().flatMap { suit ->
CardRank.values().map { rank ->
Card(suit, rank)
}
}

// FIXME: list O(n) 탐색 vs map 생성
// private val map =
// allCards
// .groupBy { it.suit }
// .mapValues { it.value.associateBy { card -> card.rank } }

fun of(suit: CardSuit, rank: CardRank): Card =
allCards.find { it.suit == suit && it.rank == rank } ?: throw IllegalArgumentException(INVALID_CARD_ERROR)
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/domain/card/CardDeck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack.domain.card

class CardDeck {
private val cards =
Card.allCards
.shuffled()
.toMutableList()

fun size(): Int = cards.size
fun draw(): Card = cards.removeLast()
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/card/CardRank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain.card

enum class CardRank(val value: Int, val forOutput: String) {
ACE(1, "A"),
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(10, "J"),
QUEEN(10, "Q"),
KING(10, "K");
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/card/CardSuit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain.card

enum class CardSuit(val forOutput: String) {
SPADE("스페이드"),
HEART("하트"),
DIAMOND("다이아몬드"),
CLUB("클로버");
}
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.domain.player

import blackjack.domain.card.Card
import blackjack.domain.card.CardDeck

class Dealer {
val cardDeck: CardDeck = CardDeck()

fun draw(): Card {
return cardDeck.draw()
}

fun deal(player: Player) {
player.addCard(draw())
}
}
31 changes: 31 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package blackjack.domain.player

import blackjack.domain.card.Card
import blackjack.domain.card.CardRank
import blackjack.domain.state.PlayerState

class Player(
val name: String = "player"
) {
var state: PlayerState = PlayerState.PLAYING
get() {
if (field == PlayerState.PLAYING && sum() > 21) field = PlayerState.BURST
return field
}
private set

private val _cards = mutableListOf<Card>()
val cards: List<Card>
get() = _cards.toList()

fun addCards(cards: List<Card>): Boolean = _cards.addAll(cards)
fun addCard(card: Card): Boolean = _cards.add(card)
fun sum(): Int {
val hasAce = cards.any { it.rank == CardRank.ACE }
val sum = cards.sumOf { it.rank.value }

return if (hasAce && sum + 10 <= 21) sum + 10 else sum
}

fun stopDraw() { state = PlayerState.STOP }
}
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/domain/state/PlayerState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack.domain.state

enum class PlayerState {
PLAYING,
STOP,
BURST
}
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/view/InputNames.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package blackjack.view

data class InputNames(
val inputString: String
) {

fun parseNames(): List<String> {
return inputString.split(",")
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package blackjack.view

import blackjack.domain.player.Player

object InputView {
private const val INPUT_PLAYERS_NAME = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"
private val INPUT_DRAW_RESPONSE =
{ playerName: String -> "${playerName}는 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)" }

fun inputPlayers(): List<Player> {
val playerNames = inputPlayerNames()

return playerNames.map { Player(it) }
}

private fun inputPlayerNames(): List<String> {
println(INPUT_PLAYERS_NAME)
val inputNames = InputNames(readln())

return inputNames.parseNames()
}

fun inputDrawResponse(player: Player): Boolean {
println(INPUT_DRAW_RESPONSE(player.name))
val response = readln()
require(response == "y" || response == "n") { "응답은 y, n만 가능합니다" }

return response == "y"
}
}
34 changes: 34 additions & 0 deletions src/main/kotlin/blackjack/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package blackjack.view

import blackjack.domain.BlackjackGame
import blackjack.domain.player.Player
import blackjack.domain.card.Card

object OutputView {

fun printInitGame(blackJackGame: BlackjackGame) {
printPlayersName(blackJackGame)
blackJackGame.players.forEach { player -> printCardsInHandWithEmptyLine(player) }
}

private fun printPlayersName(blackJackGame: BlackjackGame) {
println(blackJackGame.players.joinToString(transform = Player::name) + "에게 2장의 카드를 나누었습니다.")
}

fun printCardsInHandWithEmptyLine(player: Player) {
printPlayerCards(player)
println()
}

private fun printPlayerCards(player: Player) {
print("${player.name}카드: ")
print(player.cards.joinToString(transform = Card::print))
}

fun printResult(players: List<Player>) {
players.forEach { player ->
printPlayerCards(player)
println(" - 결과: ${player.sum()}")
}
}
}
64 changes: 64 additions & 0 deletions src/test/kotlin/blackjack/domain/BlackjackGameTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package blackjack.domain

import blackjack.domain.player.Dealer
import blackjack.domain.player.Player
import io.kotest.inspectors.forAll
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

internal class BlackjackGameTest {
private lateinit var dealer: Dealer
private lateinit var players: List<Player>
private lateinit var blackjackGame: BlackjackGame

@BeforeEach
fun beforeEach() {
dealer = Dealer()
players = listOf(Player(), Player())
blackjackGame = BlackjackGame(dealer, players)
}

@DisplayName("플레이어는 게임 시작 시 카드 두 장을 지급받는다.")
@Test
fun initPlayerGetCards() {
val dealer = Dealer()
val players = listOf(Player(), Player())
players.forAll { player ->
player.cards.isEmpty() shouldBe true
}

val blackjackGame = BlackjackGame(dealer, players)
blackjackGame.players.forAll { player ->
player.cards.size shouldBe 2
}
}

@DisplayName("플레이어가 한명이라도 카드를 뽑을 수 있는 상태라면 isNotFinish 메서드는 true를 반환한다.")
@Test
fun isNotFinishReturnTrue() {
val dealer = Dealer()
val players = listOf(Player(), Player())

val blackjackGame = BlackjackGame(dealer, players)
val actual = blackjackGame.isNotFinished()
val expect = true

actual shouldBe expect
}

@DisplayName("플레이어가 모두 카드를 뽑을 수 없는 상태라면 isNotFinish 메서드는 false를 반환한다.")
@Test
fun isNotFinishReturnFalse() {
val dealer = Dealer()
val players = listOf(Player(), Player())
players.forEach(Player::stopDraw)

val blackjackGame = BlackjackGame(dealer, players)
val actual = blackjackGame.isNotFinished()
val expect = false

actual shouldBe expect
}
}
17 changes: 17 additions & 0 deletions src/test/kotlin/blackjack/domain/card/CardDeckTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain.card

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

internal class CardDeckTest {

@DisplayName("카드 뭉치의 카드 개수는 13 * 4인 52장이다.")
@Test
fun cardDeckSize() {
val actual = CardDeck().size()
val expect = CardSuit.values().size * CardRank.values().size

actual shouldBe expect
}
}
Loading