Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# kotlin-blackjack

## Step 3
### Functional Requirements
Implement a program based on a simplified version of Blackjack. In this game, the player or dealer with a total closest to 21—without going over—wins.

- Card values follow standard Blackjack rules:
- Number cards are counted by their face value.
- Face cards (King, Queen, Jack) are each worth 10.
- Aces can be worth either 1 or 11.
- Each player starts with two cards.
- Players may draw additional cards as long as their total remains 21 or less.
- The dealer must draw a card if their total is 16 or less, and must stand on 17 or more.
- If the dealer busts (goes over 21), all remaining players automatically win.
- After the game ends, display the result (win/loss) for each player.

## Step 2
### Functional Requirements
- Card values follow standard Blackjack rules:
Expand All @@ -8,10 +23,35 @@
- At the start of the game, each player receives two cards.
- Players can choose to draw additional cards as long as their total does not exceed 21.

### Player
### Participant
- [x] Have hand
- [x] Have name
- [x] Add card to Hand
- [x] Open Card for first

### Dealer
- [x] Implement Participant
- [x] Add card to Hand

### Player
- [x] Implement Participant
- [x] Have name

### FirstTurn
- [x] Draw cards
- [x] When the sum is 21 return Blackjack
- [x] WHen the sum is less than 21 return Hit

### Hit
- [x] Draw Card
- [x] When the sum is over than 21 return Bust
- [x] When the sum is less than 21 return Hit
- [x] return stay

### Blackjack

### Bust

### Stay

### Hand
- [x] Have cards as a list
Expand All @@ -21,6 +61,7 @@
- [x] Return size
- [x] Have to have at least two cards
- [x] Return is bust
- [x] Return is blackjack

### PlayingCard
- [x] Has suit and denomination
Expand All @@ -45,4 +86,9 @@
### OutputView
- [x] Display player's cards
- [x] Display player's cards and total score
- [x] Display first turn
- [x] Display first turn
- [x] Display final results

### WinningResult
- [x] Calculate winners between two participants
- [x] Return the result of participant
58 changes: 49 additions & 9 deletions src/main/kotlin/Casino.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import card.Deck
import card.PlayingCard
import participant.Dealer
import participant.Participant
import participant.Player
import state.Bust
import state.FirstTurn
import view.InputView
import view.OutputView

Expand All @@ -8,22 +15,55 @@ class Casino(
fun run() {
val deck = Deck(PlayingCard.ALL.shuffled())
val names = inputView.getPlayerNames()
val players = names.map { Player(it, Hand(deck.drawCard(2))) }
outputView.printFirstTurn(players)
val players = names.map { Player(it, FirstTurn(Hand(emptyList()))) }
val dealer = Dealer(state = FirstTurn(Hand(emptyList())))

players.forEach { turn(it, deck) }
players.forEach { outputView.printScore(it) }
val participants: List<Participant> = players + dealer
repeat(2) { participants.forEach { it.drawCard(deck.drawOne()) } }

outputView.printFirstTurn(participants)

participants.forEach { turn(it, deck) }
participants.forEach { outputView.printScore(it) }

val winningResult = WinningResult()
participants.forEach {
when (it) {
Copy link

Choose a reason for hiding this comment

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

How do you think this conditional could be refactored?

Copy link
Author

Choose a reason for hiding this comment

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

I think it's also possible by using polymorphism like above!

is Player -> {
val result = winningResult.versus(dealer, it)
outputView.printResult(it, result)
}
is Dealer -> {
outputView.printDealerResult(it, winningResult.getResult())
}
}
}
}

private fun turn(
player: Player,
participant: Participant,
deck: Deck,
) {
while (true) {
val response = inputView.getResponse(player.name)
if (!response || player.hand.isBust()) return
player.drawCard(deck.drawCard(1).first())
outputView.printPlayerCards(player)
when (participant) {
Copy link

Choose a reason for hiding this comment

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

Would it be possible to eliminate this conditional by using polymorphism?

is Player -> {
val response = inputView.getResponse(participant.name)
if (!response) {
participant.stay()
return
}
}
is Dealer -> {
if (participant.score() > 17) {
participant.stay()
return
}
outputView.printDealerDrawGuide()
}
}
participant.drawCard(deck.drawOne())
outputView.printPlayerCards(participant)
if (participant.state is Bust) return
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
enum class GameResult {
WIN,
LOSE,
DRAW,
;

companion object {
fun getApposite(result: GameResult): GameResult {
return when (result) {
WIN -> LOSE
DRAW -> DRAW
LOSE -> WIN
}
}
}
}
14 changes: 8 additions & 6 deletions src/main/kotlin/Hand.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
class Hand(cards: List<PlayingCard>) {
init {
require(cards.size >= MINIMUM_SIZE) { ERROR_MINIMUM_SIZE }
}
import card.Denomination
import card.PlayingCard

class Hand(cards: List<PlayingCard>) {
private val _cards: MutableList<PlayingCard> = cards.toMutableList()
val cards: List<PlayingCard>
get() = _cards.toList()
Expand All @@ -24,10 +23,13 @@ class Hand(cards: List<PlayingCard>) {
return score() > MAX_SCORE
}

fun isBlackjack(): Boolean {
return size == BLACKJACK_SIZE && score() == MAX_SCORE
}

companion object {
private const val BONUS = 10
private const val MINIMUM_SIZE = 2
private const val MAX_SCORE = 21
private const val ERROR_MINIMUM_SIZE = "Have to have at least two cards"
private const val BLACKJACK_SIZE = 2
}
}
5 changes: 0 additions & 5 deletions src/main/kotlin/Player.kt

This file was deleted.

45 changes: 45 additions & 0 deletions src/main/kotlin/WinningResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import GameResult.Companion.getApposite
import GameResult.DRAW
import GameResult.LOSE
import GameResult.WIN
import participant.Participant
import state.Blackjack
import state.Bust
import state.Stay

class WinningResult {
Copy link

Choose a reason for hiding this comment

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

Great job extracting the game result logic into WinningResult! 👍

However, I noticed there's no test coverage for this logic yet.
It would be even better to add tests to make sure this logic is well covered.

private val dealerResult: MutableMap<GameResult, Int> =
mutableMapOf(
WIN to 0,
LOSE to 0,
DRAW to 0,
)

fun getResult() = dealerResult.toMap()

fun versus(
dealer: Participant,
player: Participant,
): GameResult {
val result = determineResult(dealer, player)
dealerResult[result] = dealerResult.getValue(result) + 1
return getApposite(result)
}

private fun determineResult(
dealer: Participant,
player: Participant,
): GameResult {
return when (dealer.state) {
is Bust -> LOSE
is Blackjack -> if (player.state is Blackjack) DRAW else WIN
is Stay ->
when {
dealer.score() < player.score() -> LOSE
dealer.score() == player.score() -> DRAW
else -> WIN
}
else -> throw IllegalStateException()
}
}
}
6 changes: 4 additions & 2 deletions src/main/kotlin/Deck.kt → src/main/kotlin/card/Deck.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package card

class Deck(cards: List<PlayingCard>) {
private val _cards = cards.toMutableList()
val cards: List<PlayingCard>
get() = _cards.toList()

fun drawCard(count: Int): List<PlayingCard> {
return List(count) { _cards.removeFirst() }
fun drawOne(): PlayingCard {
return _cards.removeFirst()
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package card

enum class Denomination(val score: Int) {
ACE(1),
TWO(2),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package card

data class PlayingCard private constructor(val suit: Suit, val denomination: Denomination) {
companion object {
val ALL =
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/Suit.kt → src/main/kotlin/card/Suit.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package card

enum class Suit {
CLUB,
DIAMOND,
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/participant/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package participant

import card.PlayingCard
import state.State

class Dealer(name: String = "Dealer", override var state: State) : Participant(name) {
override fun showCardFirst(): List<PlayingCard> {
return listOf(state.hand.cards.first())
Copy link

Choose a reason for hiding this comment

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

This line reaches through multiple objects, which slightly violates the Law of Demeter.
You might consider exposing a function to encapsulate this access and make the intention clearer!

This is similar to what I mentioned in a previous comment: #833 (comment)

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

import card.PlayingCard
import state.State

abstract class Participant(val name: String) {
abstract var state: State

abstract fun showCardFirst(): List<PlayingCard>

fun score(): Int {
return state.hand.score()
}

fun stay() {
state = state.stay()
}

fun drawCard(card: PlayingCard) {
state = state.drawCard(card)
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/participant/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package participant

import card.PlayingCard
import state.State

class Player(name: String, override var state: State) : Participant(name) {
override fun showCardFirst(): List<PlayingCard> {
return state.hand.cards
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/state/Blackjack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package state

import Hand

class Blackjack(hand: Hand) : Finished(hand)
5 changes: 5 additions & 0 deletions src/main/kotlin/state/Bust.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package state

import Hand

class Bust(hand: Hand) : Finished(hand)
14 changes: 14 additions & 0 deletions src/main/kotlin/state/Finished.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package state

import Hand
import card.PlayingCard

abstract class Finished(override val hand: Hand) : State {
override fun drawCard(card: PlayingCard): State {
throw IllegalStateException()
}

override fun stay(): State {
return this
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/state/FirstTurn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package state

import Hand
import card.PlayingCard

class FirstTurn(override val hand: Hand) : State {
override fun drawCard(card: PlayingCard): State {
hand.add(card)

if (hand.size == 2) {
if (hand.isBlackjack()) return Blackjack(hand)
return Hit(hand)
}
return FirstTurn(hand)
}

override fun stay(): State {
throw IllegalStateException()
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/state/Hit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package state

import Hand
import card.PlayingCard

class Hit(override val hand: Hand) : State {
override fun drawCard(card: PlayingCard): State {
hand.add(card)

return if (hand.isBust()) {
return Bust(hand)
} else {
Hit(hand)
}
}

override fun stay(): State {
return Stay(hand)
}
}
Loading