February 11, 2025
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.
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 ! ! '..' !
*_____________________________* *_____________________________*
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.
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?"
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
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.
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!