diff --git a/README.md b/README.md index b7ca2177d..5395cd4fb 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,66 @@ introduce { "English" level 3 } } -``` \ No newline at end of file +``` + +### Step2: 블랙잭 + +##### `규칙` +- 블랙잭 게임을 변형한 프로그램을 구현한다. +- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. +- 카드의 숫자 계산은 카드 숫자를 기본, +- 예외로 Ace는 1 또는 11로 계산할 수 있으며, +- King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, +- 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. +- 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. + +##### `실행 결과` +```text +게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리) +pobi,jason + +pobi, jason에게 2장의 나누었습니다. +pobi카드: 2하트, 8스페이드 +jason카드: 7클로버, K스페이드 + +pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +y +pobi카드: 2하트, 8스페이드, A클로버 +pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +n +jason은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +n +jason카드: 7클로버, K스페이드 + +pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21 +jason카드: 7클로버, K스페이드 - 결과: 17 +``` + +##### `요구사항 정리` +- 카드 + - [ ] 카드는 각각 총 13개의 랭크와, 4개의 무늬(mark or suits)가 있다 + - [x] rank : 2, 3, 4, 5, 6, 7, 8, 9, 10, A(Ace), K(King), Q(Queen), J(Jack) + - [x] suits : spade(♠️), heart(🖤), diamond(♦️), club(♣️) + - [x] rank 는 포인트를 가진다 + - [x] 숫자 : 각 숫자, Ace : 1 또는 11, King, Queen, Jack : 10 + - [x] 총 52개의 카드가 무작위 순서로 생성 할 수 있다 + - [x] 카드들의 포인트를 더할 수 있다 +- 플레이어 + - [x] 플레이어는 카드를 추가 할 수 있다 + - [x] 플레이어의 카드의 포인트를 합산 할 수 있다 + - [x] 카드 드로우를 중지하면 더이상 카드를 받을 수 없다 + - [x] 쉼표로 구분된 이름 문자열을 전달하면 플레이어를 생성 할 수 있다 +- 컨트롤러 + - [x] 게임을 시작하면 게임에 참여할 사람들 입력 요청한다 + - [x] 게임을 시작하면 무작위 전체 카드를 생성한다 + - [x] 게임 참여자들에게 2장씩 카드를 지급한다 + - [x] 모든 참여자가 카드 추가 지급을 거절하면 게임을 종료하고 합산결과 출력 요청한다 +- InputView + - [x] 게임에 참여하는 사람을 직접 입력 받는다 + - [x] 카드를 더 받을지 입력받는다 +- OutputView + - [x] 게임 참여자들에게 처음 지급된 카드 리스트를 출력한다 + - [x] 모든 사용자의 카드 리스트와 포인트 합산 결과를 출력한다 + - [x] 카드를 더 받으면 지급된 카드와 함께 전체 카드 리스트를 출력한다 + \ No newline at end of file diff --git a/src/main/kotlin/blackjack/BlackJackApplcation.kt b/src/main/kotlin/blackjack/BlackJackApplcation.kt new file mode 100644 index 000000000..e6f78396f --- /dev/null +++ b/src/main/kotlin/blackjack/BlackJackApplcation.kt @@ -0,0 +1,18 @@ +package blackjack + +import blackjack.domain.BlackJackGame +import blackjack.domain.GameCards +import blackjack.view.InputView +import blackjack.view.OutputView + +fun main() { + val players = InputView.enterParticipatingPlayers() + val gameCards = GameCards.create() + val blackJackGame = BlackJackGame(players, gameCards) + blackJackGame.startGame() + OutputView.printFirstAllPlayersCards(players) + players.forEach { player -> + blackJackGame.handlePlayerDraw(player, gameCards) + } + OutputView.printFinalResults(players) +} diff --git a/src/main/kotlin/blackjack/UserCards.kt b/src/main/kotlin/blackjack/UserCards.kt new file mode 100644 index 000000000..31ffc45fd --- /dev/null +++ b/src/main/kotlin/blackjack/UserCards.kt @@ -0,0 +1,19 @@ +package blackjack + +import blackjack.domain.Card +import blackjack.domain.Ranks + +class UserCards(private val cards: MutableList) : Collection by cards { + fun calculatePoints(): Int { + val basePoints = cards.sumOf { it.rank.points[0] } + val hasAce = cards.any { it.rank == Ranks.ACE } + if (hasAce && basePoints + 10 <= 21) { + return basePoints + 10 + } + return basePoints + } + + fun addCard(card: Card) { + cards.add(card) + } +} diff --git a/src/main/kotlin/blackjack/domain/BlackJackGame.kt b/src/main/kotlin/blackjack/domain/BlackJackGame.kt new file mode 100644 index 000000000..5f9bfd9cd --- /dev/null +++ b/src/main/kotlin/blackjack/domain/BlackJackGame.kt @@ -0,0 +1,42 @@ +package blackjack.domain + +import blackjack.view.InputView +import blackjack.view.OutputView + +class BlackJackGame( + private val players: Players, + private val gameCards: GameCards, +) { + fun startGame() { + repeat(FIRST_ROUND_HAND_SIZE) { + giveOutCards() + } + } + + private fun giveOutCards() { + players.forEach { players -> + players.receiveCard(gameCards.drawCard()) + } + } + + fun handlePlayerDraw( + player: Player, + gameCards: GameCards, + ) { + val previousCardCount = player.cardSize() + InputView.enterIsContinueDrawCard(player) + if (player.isDrawContinue) { + player.receiveCard(gameCards.drawCard()) + OutputView.printPlayerCard(player) + handlePlayerDraw(player, gameCards) // 재귀 호출 + } + + if (!player.isDrawContinue && player.cardSize() == previousCardCount) { + OutputView.printPlayerCard(player) + } + } + + companion object { + private const val FIRST_ROUND_HAND_SIZE = 2 + } +} diff --git a/src/main/kotlin/blackjack/domain/Card.kt b/src/main/kotlin/blackjack/domain/Card.kt new file mode 100644 index 000000000..d5632d54a --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Card.kt @@ -0,0 +1,3 @@ +package blackjack.domain + +data class Card(val rank: Ranks, val suits: Suits) diff --git a/src/main/kotlin/blackjack/domain/GameCards.kt b/src/main/kotlin/blackjack/domain/GameCards.kt new file mode 100644 index 000000000..853b36e17 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/GameCards.kt @@ -0,0 +1,24 @@ +package blackjack.domain + +import java.util.LinkedList +import java.util.Queue + +class GameCards private constructor(private val deck: Queue) { + fun drawCard(): Card { + return deck.poll() + } + + fun size(): Int { + return deck.size + } + + companion object { + fun create(): GameCards { + val allCards = + Suits.entries.flatMap { suit -> + Ranks.entries.map { rank -> Card(rank, suit) } + } + return GameCards(LinkedList(allCards.shuffled())) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Player.kt b/src/main/kotlin/blackjack/domain/Player.kt new file mode 100644 index 000000000..4de3482d9 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Player.kt @@ -0,0 +1,34 @@ +package blackjack.domain + +import blackjack.UserCards + +class Player(val name: String, var isDrawContinue: Boolean = true) { + private var userCards = UserCards(mutableListOf()) + + fun receiveCard(card: Card) { + require(isDrawContinue) { "카드를 받을 수 없습니다" } + userCards.addCard(card) + } + + fun cardSize(): Int { + return userCards.size + } + + fun calculateCardPoints(): Int { + return userCards.calculatePoints() + } + + fun stopCardDraw() { + isDrawContinue = false + } + + fun continueCardDraw() { + isDrawContinue = true + } + + fun findAllCardsNames(): List { + return userCards.map { card -> + card.rank.keyword + card.suits.koreanName + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Players.kt b/src/main/kotlin/blackjack/domain/Players.kt new file mode 100644 index 000000000..bc4f6412a --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Players.kt @@ -0,0 +1,16 @@ +package blackjack.domain + +data class Players(val players: List) : Collection by players { + fun getPlayerNames(): List { + return players.map { it.name } + } + + companion object { + private const val DELIMITER = "," + + fun create(playerNames: String): Players { + val players = playerNames.split(DELIMITER).map { playerName -> Player(playerName) } + return Players(players) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/Ranks.kt b/src/main/kotlin/blackjack/domain/Ranks.kt new file mode 100644 index 000000000..e8018be27 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Ranks.kt @@ -0,0 +1,20 @@ +package blackjack.domain + +enum class Ranks( + val keyword: String, + val points: List, +) { + ACE("A", listOf(1, 11)), + TWO("2", listOf(2)), + THREE("3", listOf(3)), + FOUR("4", listOf(4)), + FIVE("5", listOf(5)), + SIX("6", listOf(6)), + SEVEN("7", listOf(7)), + EIGHT("8", listOf(8)), + NINE("9", listOf(9)), + TEN("10", listOf(10)), + JACK("J", listOf(10)), + QUEEN("Q", listOf(10)), + KING("K", listOf(10)), +} diff --git a/src/main/kotlin/blackjack/domain/Suits.kt b/src/main/kotlin/blackjack/domain/Suits.kt new file mode 100644 index 000000000..d1fba6583 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/Suits.kt @@ -0,0 +1,10 @@ +package blackjack.domain + +enum class Suits( + val koreanName: String, +) { + SPADE("스페이드"), + HEART("하트"), + DIAMOND("다이어"), + CLUB("클로버"), +} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt new file mode 100644 index 000000000..8d5d95c72 --- /dev/null +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -0,0 +1,29 @@ +package blackjack.view + +import blackjack.domain.Player +import blackjack.domain.Players + +object InputView { + private const val CONTINUE_OR_STOP_MESSAGE = "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)" + + fun enterParticipatingPlayers(): Players { + println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") + val playerNames = readln() + return Players.create(playerNames) + } + + fun enterIsContinueDrawCard(player: Player) { + val userInput = getIsContinueDraw(player) + when (userInput.lowercase()) { + "n" -> player.stopCardDraw() + "y" -> player.continueCardDraw() + else -> println("잘못된 입력입니다. 다시 입력해주세요.") + } + } + + fun getIsContinueDraw(player: Player): String { + println() + println(player.name + CONTINUE_OR_STOP_MESSAGE) + return readln() + } +} diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt new file mode 100644 index 000000000..93a5bf1a6 --- /dev/null +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -0,0 +1,51 @@ +package blackjack.view + +import blackjack.domain.Player +import blackjack.domain.Players + +object OutputView { + private const val PLAYER_NAME_DELIMITER = ", " + private const val BLANK_PREFIX_MESSAGE = "" + private const val POST_MESSAGE = "에게 2장의 나누었습니다." + private const val RESULT_EXPRESSION = " - 결과: " + private const val NAME_POSTFIX_EXPRESSION = "카드: " + + fun printFirstAllPlayersCards(players: Players) { + println() + val result = + players.getPlayerNames().joinToString( + PLAYER_NAME_DELIMITER, + BLANK_PREFIX_MESSAGE, + POST_MESSAGE, + ) + println(result) + players.forEach { player -> + printPlayerAllCards(player) + println() + } + } + + fun printFinalResults(players: Players) { + println() + println() + println("게임 종료!") + players.forEach { player -> + printPlayerAllCards(player) + resultExpression(player) + } + } + + fun printPlayerCard(player: Player) { + printPlayerAllCards(player) + } + + private fun printPlayerAllCards(player: Player) { + print(player.name + NAME_POSTFIX_EXPRESSION) + print(player.findAllCardsNames().joinToString(PLAYER_NAME_DELIMITER)) + } + + private fun resultExpression(player: Player) { + print(RESULT_EXPRESSION) + println(player.calculateCardPoints()) + } +} diff --git a/src/test/kotlin/blackjack/BlackJackTest.kt b/src/test/kotlin/blackjack/BlackJackTest.kt new file mode 100644 index 000000000..7c7a4dcb1 --- /dev/null +++ b/src/test/kotlin/blackjack/BlackJackTest.kt @@ -0,0 +1,105 @@ +package blackjack + +import blackjack.domain.BlackJackGame +import blackjack.domain.Card +import blackjack.domain.GameCards +import blackjack.domain.Player +import blackjack.domain.Players +import blackjack.domain.Ranks +import blackjack.domain.Suits +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class BlackJackTest { + @Test + fun `에이스가 아닌 카드의 포인트를 합산 할 수 있다`() { + val card1 = Card(rank = Ranks.TWO, suits = Suits.SPADE) + val card2 = Card(rank = Ranks.SIX, suits = Suits.HEART) + val userCards = UserCards(mutableListOf(card1, card2)) + userCards.calculatePoints() shouldBe 8 + } + + @Test + fun `포인트를 합산 할때 21보다 크지 않다면 에이스 카드는 11점으로 계산한다`() { + val card1 = Card(rank = Ranks.ACE, suits = Suits.SPADE) + val card2 = Card(rank = Ranks.TEN, suits = Suits.HEART) + val userCards = UserCards(mutableListOf(card1, card2)) + userCards.calculatePoints() shouldBe 21 + } + + @Test + fun `포인트를 합산할때 21점을 초과하면 에이스 카드는 1점으로 계산한다`() { + val card1 = Card(rank = Ranks.ACE, suits = Suits.SPADE) + val card2 = Card(rank = Ranks.EIGHT, suits = Suits.HEART) + val card3 = Card(rank = Ranks.TEN, suits = Suits.HEART) + val userCards = UserCards(mutableListOf(card1, card2, card3)) + userCards.calculatePoints() shouldBe 19 + } + + @Test + fun `전체 카드의 수는 52이다`() { + val gameCards = GameCards.create() + gameCards.size() shouldBe 52 + } + + @Test + fun `게임 카드를 모두 드로우 하면 전체 덱의 사이즈는 0이다`() { + val gameCards = GameCards.create() + repeat(gameCards.size()) { + gameCards.drawCard() + } + gameCards.size() shouldBe 0 + } + + @Test + fun `플레이어는 드로우 된 카드를 가질 수 있다`() { + val player = Player("lee") + val gameCards = GameCards.create() + val card1 = gameCards.drawCard() + player.receiveCard(card1) + val card2 = gameCards.drawCard() + player.receiveCard(card2) + player.cardSize() shouldBe 2 + } + + @Test + fun `플레이어의 카드 점수를 합산 할 수 있다`() { + val player = Player("lee") + player.receiveCard(Card(rank = Ranks.TEN, suits = Suits.CLUB)) + player.receiveCard(Card(rank = Ranks.TWO, suits = Suits.DIAMOND)) + player.receiveCard(Card(rank = Ranks.FIVE, suits = Suits.HEART)) + player.calculateCardPoints() shouldBe 17 + } + + @Test + fun `카드 드로우 중지 요청하면 플레이어는 카드를 받을수 없다`() { + val player = Player("lee") + player.stopCardDraw() + shouldThrow { + player.receiveCard(Card(rank = Ranks.SIX, suits = Suits.CLUB)) + }.also { + it.message shouldBe "카드를 받을 수 없습니다" + } + } + + @Test + fun `게임에 참여할 사람들의 이름을 입력 받을 수 있다`() { + val players = Players.create("kim,lee,hong") + players.size shouldBe 3 + players.getPlayerNames() shouldBe listOf("kim", "lee", "hong") + } + + @Test + fun `게임을 시작하면 각 플레이어들은 2장씩 카드를 지급 받는다`() { + val players = Players.create("kim,lee,hong") + val gameCards = GameCards.create() + val game = BlackJackGame(players, gameCards) + game.startGame() + players.size shouldBe 3 + players.sumOf { player -> player.cardSize() } shouldBe 6 + players.forEach { + it.cardSize() shouldBe 2 + } + } +}