Command-line 'Set' Game in Kotlin 2

February 11, 2025

Part 2: Rendering Cards in Ascii Nostalgically

The Theory Behind the Ascii Rendering

So, as I toyed with my rendering code I realized I needed a way to define a Symbol and then print it from side to side mulitple times. I realized this could all be put to a pattern inside of a card and adding edges. Eventually I wound up realizing that the kotlin string output didn't handle groups of spaces well, so I needed a fill character. From there, I asked gpt how to make color codes in terminal, and realized I needed those everywhere. 😅 Took a lot to get the custom cards to print, but I'll explain.

What a Symbol Looks Like in Code

fun getSquiggle(c: ColorCode, f: FillCode, e: ColorCode = BG_COLOR): AsciiShape = """
$c  .''.   $e
$c  \$f$f$f\  $e
$c  /$f$f$f/  $e
$c /$f$f$f\   $e
$c  \$f$f$f\  $e
$c  /$f$f$f/  $e
$c  '..'   $e
"""

fun getDiamond(c: ColorCode, f: FillCode, e: ColorCode = BG_COLOR): AsciiShape = """
$c    A    $e
$c   /$f\   $e
$c  /$f$f$f\  $e
$c <$f$f$f$f$f> $e
$c  \$f$f$f/  $e
$c   \$f/   $e
$c    V    $e
"""

fun getOval(c: ColorCode, f: FillCode, e: ColorCode = BG_COLOR):AsciiShape = """
$c .-'''-. $e
$c |$f$f$f$f$f| $e
$c |$f$f$f$f$f| $e
$c |$f$f$f$f$f| $e
$c |$f$f$f$f$f| $e
$c |$f$f$f$f$f| $e
$c '-___-' $e
"""

Drawing the Given Number of Symbols for a Card

fun drawCard2(shape: Shape, color: Color, fill: Fill): String {
    val colorCode = getColorCode(color)
    val fillCode = getFillCode(fill)
    val combineVertical = combineVertically(
        noLog = true,
        leftCard,
        emptySpace(),
        emptySpace(),
        emptySpace(),
        getShape(shape, colorCode, fillCode),
        emptySpace(),
        emptySpace(),
        emptySpace(),
        getShape(shape, colorCode, fillCode),
        emptySpace(),
        emptySpace(),
        emptySpace(),
        rightCard
    )
    return """
${getTopCard()}
$combineVertical
${getBottomCard()}
"""
}

The way this works is by using a vertically combined string! But how is that even possible. Luckily, this is a task GPT was great for, with some finagling I was able to prompt it "how can I combine strings in kotlin vertically" and get a working response. From there I refactored it a bit and came up with this helper function behind the core of the renderings.

fun combineVertically(noLog: Boolean? = false, vararg strings: String): String {
    var firstSize = 0;
    fun logPredicate(list: List<List<String>>):Boolean {
        return list.any {
            println(it.size.toString() + " " + firstSize)
            return@any it.size != firstSize
        }
    }
    fun predicate(list: List<List<String>>) = list.any { it.size != firstSize }
    val linesList = strings.map { it.trimIndent().lines() }
        .apply {
            firstSize = first().size
            val badNumLines = if(noLog == true) { predicate(this) } else { logPredicate(this) }
            if(badNumLines) { throw IllegalArgumentException("Line Length Error") }
        }
    return (0 until firstSize).joinToString("\n") { i ->
        linesList.joinToString(separator = "") { it[i] }
    }
}

Asking GPT for color codes

This keeps it simple for us, we simply wrap our printed output with these codes and we're all set for color

typealias ColorCode = String
object ConsoleColor {
    const val RED: ColorCode = "\u001B[31m"
    const val GREEN: ColorCode = "\u001B[32m"
    const val BLUE: ColorCode = "\u001B[34m"
    const val WHITE: ColorCode = "\u001B[37m"
}

Now we have all of the characteristics we need to build the game! Pretty Neat!

Putting the card together

To keep things fully decoupled I ended up duplicating my symbols and adding mapping code to each card. The mapping code takes in my representation as an enum of the internal symbol I've used for my mini-deck. This way I'm coding against true types the entire time and I can keep my decoupled rendering clean (this is at the trade-off of boilerplate, but I think this is simple to follow regardless).

fun getShape(shape: Shape, colorCode: ColorCode, fillCode: FillCode) =
    when (shape) {
        Shapes.Squiggle -> getSquiggle(colorCode, fillCode)
        Shapes.Diamond -> getDiamond(colorCode, fillCode)
        Shapes.Oval -> getOval(colorCode, fillCode)
        else -> throw Error("No matching shape code")
    }

fun getColorCode(color: Color) =
    when(color){
        Colors.Red -> RED
        Colors.Blue -> BLUE
        Colors.Green -> GREEN
        else -> throw Error("No matching color code")
    }

fun getFillCode(fill: Fill) =
    when(fill){
        Fills.Full -> FULL
        Fills.Light -> EMPTY
        Fills.Shaded -> MED
        else -> throw Error("No matching color code")
    }

fun getAsciiCard(card: Card): AsciiCard =
    when(card.count){
        Counts.`1` -> drawCard1(card.shape, card.color, card.fill)
        Counts.`2` -> drawCard2(card.shape, card.color, card.fill)
        Counts.`3` -> drawCard3(card.shape, card.color, card.fill)
        else -> throw Error("No matching shape count")
    }

Notice get ascii card takes in a Card type and returns AsciiCard. This will make life much easier as we debug our logic and go back and forth between our internal and external representations (more on this later).

Rendering and Displaying the cards

Note: The full example is available on github under "MainCardDrawing.kt"

To render out a full game of Ascii set cards, all we do is create our card map, shuffle and deal, then map each card to it's representation.

fun main(){
    val cardsDealt = createHashMap()
        .shuffleAndDeal12()
    
    val outputString = dealPlayableCards(
        cardsDealt.map{
            val card = it.value.first
            getAsciiCard(card)
        }
    )
    print(outputString)
}

The Finished Output

The finished game output

Nice! But how do we know what is a set or not?

© 2025Brooks DuBois™