February 11, 2025
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.
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
"""
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] }
}
}
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!
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).
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)
}
Nice! But how do we know what is a set or not?