Command-line 'Set' Game in Kotlin 1

February 11, 2025

Part 1: Building an Enumerated "Set" Deck

Set: The Classic Card Game

Set is a game where you have three different types of shapes, ∿ squiggles, ♦️ Diamonds, and ⬮ Ovals, colors, red, green, & blue, count, 1, 2, & 3, and fill, empty, medium, full These make up 3^4 (81) total cards. The cards are dealt in a 4x3 grid.

Your goal is to match either 3 types by combining cards by each card's shape, color and count. The characteristics must be either all the same or all different for 3 cards which form a set. For instance, three red squiggles, two red squiggles, and one red squiggle form a set.

Set the game

Command Line Set

I decided to try and create a set engine leveraging GPT here and there for the heavy lifting. I figured, why not make it a command line game to test out my 'engine', and I can use ASCII graphics. After some prototyping:

ie. minimal?
 _____   _____   _____
| $SS | | ⬮⬮⬮ | | ♦♦♦ |  (no fill)
|_____| |_____| |_____| 

I decided to go for a full fledged approach:

 _____________________________    _____________________________ 
*                             *  *                             *
!     A        A        A     !  !            .''.             !
!    /.\      /.\      /.\    !  !            \===\            !
!   /...\    /...\    /...\   !  !            /===/            !
!  <.....>  <.....>  <.....>  !  !           /===\             !
!   \.../    \.../    \.../   !  !            \===\            !
!    \./      \./      \./    !  !            /===/            !
!     V        V        V     !  !            '..'             !
*_____________________________*  *_____________________________*

During this process

While drawing ascii set cards I realized the card's states could be as minimal as: |3,G,S,D| Just a character for each characteristic.

So, the translation is 3 Green Shaded Diamonds, |3,B,S,O| would be 3 Red Shaded Ovals, etc. Then, if I decouple my code well enough, it wont matter how I'm rendering.

Initial Types

typealias CardFaceValue = String

typealias Shape = CardFaceValue
typealias Count = CardFaceValue
typealias Fill = CardFaceValue
typealias Color = CardFaceValue

data class Card(val shape: Shape, val color: Color, val fill: Fill, val count: Count)

"Why not just use everything as a String, Brooks?"

  • This enumerate aliasing makes it easier to explain and keep track of things in the debugger.
  • Leveraging type safety at a very granular level makes code more explicit and clear to the reader.
  • Everything String?, it's boring.

For an entire board. This decomposes prettly simply in kotlin into self expalanatory code

Once I set up my first type aliases and class, it became easy to enumerate object's

Kotlin objects for each characteristic

object Shapes {
    const val Squiggle: Shape = "S"
    const val Oval: Shape = "O"
    const val Diamond: Shape = "D"
}

object Fills {
    const val Light: Fill = "L"
    const val Shaded: Fill = "S"
    const val Full: Fill = "F"
}

object Colors {
    const val Red: Color = "R"
    const val Green: Color = "G"
    const val Blue: Color = "B"
}

object Counts {
    const val `1`: Count = "1"
    const val `2`: Count = "2"
    const val `3`: Count = "3"
}

To make everything print nicely in minimal form I added a toString extension

const val sep = ","
const val endLine = "|"

typealias CardSerialized = String
fun Card.toCardStr(): CardSerialized =
    "$endLine$count$sep$color$sep$fill$sep$shape$endLine"

By now I had turned to GPT for validation. I also asked it about math of the game, which was pretty interesting. Turns out that as you add cards and characteristics to the board, it grows super-exponentially fast how many sets there are therefore the game complexity increases but the game get's easier to play. I decided to make all of these rules flexible as I proceed.

Building out the Set deck

So, asking GPT that given this setup, how do I deal some hands? It gave me a strange answer at first but the card enumeration it came up with is pretty simple, all we're doing is looping through each using a 'getAll' method we add to our oubjects. For instance, Shapes now becomes:

object Shapes {
    const val Squiggle: Shape = "S"
    const val Oval: Shape = "O"
    const val Diamond: Shape = "D"
    
    fun getShapes():List<Shape> =
        listOf(Oval, Diamond, Squiggle)

}

After adding a getter for the options, from there we can access our shapes statically and loop through them all. This adds the complexity we expect 3^4. The only added coded I have here is to push on the minimal card as a key so we can work with it in memory. This way we can decouple our rendered card from our key of our Card and work with the hashed key to process our logic. It will be a simple string representation or an internal setup of Types and Objects.

typealias CardIndex = Int
typealias CardHashMap = MutableMap<CardIndex, Pair<Card, CardSerialized>>
fun createHashMap(): CardHashMap {
    val cardHashMap: CardHashMap = mutableMapOf()
    Shapes.getShapes().forEachIndexed { shapeIndex, shape ->
        Counts.getCounts().forEachIndexed { countIndex, count ->
            Fills.getFills().forEachIndexed { filIndex, fill ->
                Colors.getColors().forEachIndexed { colorIndex, color ->
                    val card = Card(shape, color, fill, count)
                    val cardStr = card.toCardStr()
                    val cardIndex = "${shapeIndex+1}${countIndex+1}${filIndex+1}${colorIndex+1}".toInt()
                    cardHashMap[cardIndex] = Pair(card, cardStr)
                }
            }
        }
    }
    return cardHashMap
}

Now the actual shuffle and dealing of the entire card Set becomes easy across entries.

fun CardHashMap.shuffleAndDeal12() = this.entries.shuffled().take(12)
fun CardHashMap.shuffle() = this.entries.shuffled()

I've included a main method for trying this out.

fun main() {
    val dealCards = createHashMap().shuffle().take(12)

    repeat(3){outerX ->
        repeat(4){innerY ->
            print(dealCards[outerX * 4 + innerY].value.second)
        }

        if(outerX != 2) println()
    }
}

If you give it a shot, it shows how simply we've leveraged kotlin to print out a set game.

|3,R,L,D||3,R,F,D||2,G,L,O||3,B,L,O|
|1,G,F,S||3,R,S,S||1,R,L,S||1,B,S,D|
|1,G,S,S||2,R,L,S||2,R,F,S||3,G,S,O|

It's possible to actually play just with this, lol, but the ascii cards would be cool. Now onto the ascii rendering!

© 2025Brooks DuBois™