Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
06e589f
refactor: try using sealed class
syoun602 Apr 13, 2025
51e5018
docs: update README.md
syoun602 Apr 13, 2025
32fa875
feat: add card domain
syoun602 Apr 13, 2025
2f146f8
feat: add deck domain
syoun602 Apr 13, 2025
daec608
feat: add cache to reusable cards
syoun602 Apr 15, 2025
8ddf6ed
feat: add deck generator
syoun602 Apr 15, 2025
3042bee
feat: add Hands
syoun602 Apr 16, 2025
52001fe
feat: add states for initial turn and hit
syoun602 Apr 16, 2025
d0612db
feat: add Bust state
syoun602 Apr 16, 2025
00fdb88
feat: add input view
syoun602 Apr 16, 2025
ef119dd
feat: add name domain
syoun602 Apr 16, 2025
bc0f2bf
refactor: rename state
syoun602 Apr 16, 2025
7975485
feat: add Name domain
syoun602 Apr 16, 2025
25f34ef
refactor: change package structure
syoun602 Apr 16, 2025
cbb5c80
feat: add draw card logic for deck
syoun602 Apr 16, 2025
aa5caee
feat: add player domain
syoun602 Apr 17, 2025
0e54eb4
chore: for save
syoun602 Apr 17, 2025
86af5df
feat: add view logics
syoun602 May 1, 2025
d89e9e3
feat: add continuable condition for state
syoun602 May 1, 2025
c728cd1
test: add test for players
syoun602 May 1, 2025
a8a6cf6
feat: add property for player on can draw card
syoun602 May 1, 2025
ae0df5b
test: add tests for states canContinue
syoun602 May 1, 2025
5166805
feat: add calculating score for ace values
syoun602 May 1, 2025
aaf4172
feat: add score for all states and player
syoun602 May 4, 2025
9e6b39b
feat: add blackjack game plays
syoun602 May 4, 2025
9eaf730
docs: update README.md
syoun602 May 4, 2025
74c14d0
feat: use kotlin dsl marker
syoun602 May 4, 2025
0670f0a
docs: update README.md
syoun602 May 4, 2025
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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# kotlin-blackjack
# kotlin-blackjack

## Blackjack

### Key objects
Card

- [x] Number cards are counted by their face value.
- [x] Aces can count as either 1 or 11.

Card Number

- [x] Number or symbol (J, Q, K, A).
- [x] Face cards (King, Queen, Jack) are each worth 10.

Card Suit

- [x] Contains four card suits (Spades, Hearts, Diamonds, Clubs).

Deck

- [x] Deck must not contain duplicated cards.

Hands
- [x] Must have two cards to be initialized.
- [x] Should be able to add card to hands.

Player

- [x] Receives two cards at start of the game.
- [x] Players can choose to draw additional cards as long as their total does not exceed 21.

### States

State
- [x] Interface for all game states

InitialTurn
- [x] Should remain initial until card size is less than or equal to 2
- [x] If hands has more than 3 cards after draw, should move state to Hit.

Hit
- [x] Can add cards until bust
31 changes: 31 additions & 0 deletions src/main/kotlin/blackjack/BlackjackApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package blackjack

import blackjack.domain.deck.Deck
import blackjack.domain.player.Name
import blackjack.domain.player.Player
import blackjack.domain.player.Players
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val rawNames = InputView.getPlayerNames()
val players = Players(rawNames.map(::Name).map { Player(it) })
val deck = Deck.create()

players.initializeState { deck.drawCard() }
OutputView.printInitialCards(players)

players.values.forEach { player -> player.takeTurn(deck) }
OutputView.printBlackjackResult(players)
}

private fun Player.takeTurn(deck: Deck) {
while (this.canDraw && InputView.getUserChoice(this)) {
this.draw(deck.drawCard())
OutputView.printPlayerCards(this)

if (!this.canDraw) {
return OutputView.announceBust(this)
}
}
}
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 number: CardNumber,
val suit: Suit,
) {
companion object {
private val cache: Map<Pair<CardNumber, Suit>, Card> =
CardNumber.entries.flatMap { number ->
Suit.entries.map { suit ->
Pair(Pair(number, suit), Card(number, suit))
}
}.toMap()

val cached: List<Card> = cache.values.toList()

fun of(
number: CardNumber,
suit: Suit,
) = cache[Pair(number, suit)] ?: throw IllegalStateException("Card does not exist in cache.")

fun of(
rawNumber: String,
rawSuit: String,
) = of(CardNumber.fromName(rawNumber), Suit.fromName(rawSuit))
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/blackjack/domain/card/CardNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package blackjack.domain.card

enum class CardNumber(val value: Int) {
ACE(1),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),
JACK(10),
QUEEN(10),
KING(10),
;

companion object {
fun fromName(name: String) =
entries.find { it.name == name.uppercase() }
?: throw IllegalArgumentException("Card number does not exist for $name")
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/card/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package blackjack.domain.card

enum class Suit {
SPADES,
HEARTS,
DIAMONDS,
CLUBS,
;

companion object {
fun fromName(name: String) =
entries.find { it.name == name.uppercase() }
?: throw IllegalArgumentException("Card suit does not exist for $name")
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/blackjack/domain/deck/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackjack.domain.deck

import blackjack.domain.card.Card

class Deck private constructor(
private val cards: MutableList<Card>,
) {
init {
check(cards.size == cards.distinct().size) {
"Deck must not contain duplicated cards."
}
}

fun drawCard(): Card {
check(cards.isNotEmpty()) { "There are no cards left in deck." }
return cards.removeFirst()
}

companion object {
fun create(generator: () -> MutableList<Card> = RandomDeckGenerator::generate) = Deck(generator())
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/domain/deck/DeckGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack.domain.deck

import blackjack.domain.card.Card

interface DeckGenerator {
fun generate(): MutableList<Card>
}

object RandomDeckGenerator : DeckGenerator {
override fun generate() = Card.cached.shuffled().toMutableList()
}
35 changes: 35 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Hands.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package blackjack.domain.player

import blackjack.domain.card.Card
import blackjack.domain.card.CardNumber

class Hands(
val cards: List<Card> = emptyList(),
) {
val initialized
get() = cards.size >= 2

val size: Int
get() = cards.size

infix operator fun plus(card: Card) = Hands(cards + card)

fun isBust(): Boolean = calculateScore() > BLACKJACK_SCORE

fun calculateScore(): Int {
val sum = cards.sumOf { it.number.value }

return when {
!hasAce() -> sum
sum + ACE_ADD_SCORE > BLACKJACK_SCORE -> sum
else -> sum + ACE_ADD_SCORE
}
}

fun hasAce() = cards.map { it.number }.contains(CardNumber.ACE)

companion object {
private const val BLACKJACK_SCORE = 21
private const val ACE_ADD_SCORE = 10
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Name.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack.domain.player

class Name(val value: String) {
init {
require(value.trim().isNotEmpty()) { "Name can't be empty." }
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package blackjack.domain.player

import blackjack.domain.card.Card
import blackjack.domain.state.InitialState
import blackjack.domain.state.State

class Player(
val name: Name,
_state: State = InitialState(),
) {
var state = _state
private set

val cards: List<Card>
get() = state.hands.cards

val canDraw: Boolean
get() = state.canContinue

val score: Int?
get() = state.score

constructor(rawName: String) : this(Name(rawName))
constructor(rawName: String, state: State) : this(Name(rawName), state)

fun draw(card: Card) {
state = state.addCard(card)
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/blackjack/domain/player/Players.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackjack.domain.player

import blackjack.domain.card.Card

class Players(
val values: List<Player>,
) {
val names = values.map { it.name.value }

constructor(vararg rawNames: String) : this(rawNames.map { Player(Name(it)) })

fun initializeState(block: () -> Card) {
repeat(FIRST_TURN_REPETITIONS) {
values.forEach { it.draw(block()) }
}
}

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

import blackjack.domain.card.Card
import blackjack.domain.player.Hands

class Bust(override val hands: Hands) : State {
override val canContinue = false

override val score: Int?
get() = null

override fun addCard(card: Card): State {
throw IllegalStateException("Can't add card after bust.")
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/domain/state/Hit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blackjack.domain.state

import blackjack.domain.card.Card
import blackjack.domain.player.Hands

class Hit(override val hands: Hands) : State {
override val score: Int?
get() = hands.calculateScore()

override fun addCard(card: Card): State {
val hands = hands + card
if (hands.isBust()) {
return Bust(hands)
}

return Hit(hands)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/domain/state/InitialState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blackjack.domain.state

import blackjack.domain.card.Card
import blackjack.domain.player.Hands

class InitialState(override val hands: Hands = Hands()) : State {
override val score: Int?
get() = hands.calculateScore()

override fun addCard(card: Card): State {
val hands = hands + card
if (!hands.initialized) {
return InitialState(hands)
}

return Hit(hands)
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/state/State.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package blackjack.domain.state

import blackjack.domain.card.Card
import blackjack.domain.player.Hands

interface State {
val hands: Hands

val canContinue: Boolean
get() = true

val score: Int?

fun addCard(card: Card): State
}
35 changes: 35 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package blackjack.view

import blackjack.domain.player.Player

object InputView {
fun getPlayerNames(): List<String> {
println("Enter the names of the players (comma-separated):")

return readln()
.split(",")
.map { it.trim() }
.filter { it.isNotBlank() }
}

fun getUserChoice(player: Player): Boolean {
println("Would ${player.name.value} like to draw another card? (y for yes, n for no)")
val input = readln()

return UserChoice.from(input).value
}
}

enum class UserChoice(val value: Boolean) {
Y(true),
N(false),
;

companion object {
fun from(input: String): UserChoice {
return entries.find {
it.name == input.uppercase()
} ?: throw IllegalArgumentException("Invalid user choice: $input")
}
}
}
Loading