Abacus/Soroban CLI in Kotlin Script

kotlin cli scripting

I was tinkering with Kotlin Script (.kts) files today and built a small interactive abacus (soroban) CLI. Kotlin scripts run with kotlinc -script or the Kotlin CLI; no project or build file required—just a shebang and you can ./Abacus.kts and go.

Demo

Commands: +<n> / add <n> to add, -<n> / sub <n> to subtract. Type help for commands. Empty line or q quits.

./Abacus.kts
  Total: 0

  ○   │○   │○   │○   
  ────┼────┼────┼────
  ○○○○│○○○○│○○○○│○○○○
  1K   100s 10s  1s

  Commands:
    +<n>  or  + <n>  or  add <n>   add n to total
    -<n>  or  - <n>  or  sub <n>   subtract n from total
    help                            show this help
    q  or  empty line               quit

How to run

Make the script executable and run it (Kotlin CLI required):

chmod +x Abacus.kts
./Abacus.kts

Or:

kotlinc -script Abacus.kts

The script

Layout: each column is one decimal digit. Upper row = 1 bead worth 5; lower row = 4 beads worth 1 each. Digit value = 5×upper + lower.

#!/usr/bin/env kotlin

/**
 * Interactive visual abacus. Commands: +<n> / add <n> to add, -<n> / sub <n> to subtract.
 * Type "help" for commands. Empty line or 'q' quits.
 */

// ─── Abacus layout: each column = one decimal digit ────────────────────────
// Upper row: 1 bead = 5  (0 or 1 bead)
// Lower row: 4 beads = 1 each (0–4 beads)
// Digit value = 5 * upper + lower

val UPPER_BEADS = 1   // one bead worth 5
val LOWER_BEADS = 4   // four beads worth 1 each
val BEAD_ON = '●'
val BEAD_OFF = '○'
val COLUMN_WIDTH = 4
val DEFAULT_DIGITS = 4   // minimum columns to show (e.g. 0 → 0000)

fun digitToBeads(d: Int): Pair<Int, Int> {
    require(d in 0..9) { "digit must be 0-9: $d" }
    val upper = d / 5
    val lower = d % 5
    return upper to lower
}

fun renderColumn(digit: Int): List<String> {
    val (upper, lower) = digitToBeads(digit)
    val upperRow = (BEAD_OFF.toString().repeat(UPPER_BEADS - upper)) + (BEAD_ON.toString().repeat(upper))
    val lowerRow = (BEAD_ON.toString().repeat(lower)) + (BEAD_OFF.toString().repeat(LOWER_BEADS - lower))
    return listOf(
        upperRow.padEnd(COLUMN_WIDTH),
        "─".repeat(COLUMN_WIDTH),
        lowerRow.padEnd(COLUMN_WIDTH)
    )
}

fun renderAbacus(total: Long, minDigits: Int = DEFAULT_DIGITS): String {
    val value = if (total < 0) 0L else total
    val digits = value.toString().map { it.digitToInt() }
    val padded = if (digits.size < minDigits) {
        List(minDigits - digits.size) { 0 } + digits
    } else digits

    val columnLines = padded.map { renderColumn(it) }
    val line0 = columnLines.joinToString("│") { it[0] }
    val line1 = columnLines.joinToString("┼") { it[1] }
    val line2 = columnLines.joinToString("│") { it[2] }

    val labels = padded.indices.reversed().map { i ->
        when (i) {
            0 -> "1s"
            1 -> "10s"
            2 -> "100s"
            3 -> "1K"
            4 -> "10K"
            5 -> "100K"
            6 -> "1M"
            7 -> "10M"
            else -> "10^$i"
        }.padEnd(COLUMN_WIDTH)
    }.joinToString(" ")

    return buildString {
        appendLine("  Total: $total")
        appendLine()
        appendLine("  $line0")
        appendLine("  $line1")
        appendLine("  $line2")
        appendLine("  $labels")
    }
}

fun isInteractive(): Boolean = System.console() != null

fun clearScreen() {
    if (isInteractive()) print("\u001b[H\u001b[2J")
}

val HELP_TEXT = """
  Commands:
    +<n>  or  + <n>  or  add <n>   add n to total
    -<n>  or  - <n>  or  sub <n>   subtract n from total
    help                            show this help
    q  or  empty line               quit
""".trimIndent()

fun parseCommand(input: String): Pair<Long, Boolean>? {
    val s = input.trim()
    val addMatch = Regex("^\\+\\s*(\\d+)$").find(s) ?: Regex("^add\\s+(\\d+)$", RegexOption.IGNORE_CASE).find(s)
    if (addMatch != null) return addMatch.groupValues[1].toLong() to true
    val subMatch = Regex("^\\-\\s*(\\d+)$").find(s) ?: Regex("^sub(?:tract)?\\s+(\\d+)$", RegexOption.IGNORE_CASE).find(s)
    if (subMatch != null) return subMatch.groupValues[1].toLong() to false
    if (s.matches(Regex("^(\\d+)$"))) return s.toLong() to true  // bare number = add
    return null
}

fun main() {
    var total = 0L

    while (true) {
        print(renderAbacus(total))
        print("\n  ")
        System.out.flush()
        val input = (System.console()?.readLine() ?: readLine())?.trim() ?: break
        if (input.equals("q", ignoreCase = true) || input.isEmpty()) break
        if (input.equals("help", ignoreCase = true)) {
            println(HELP_TEXT)
            continue
        }
        val parsed = parseCommand(input)
        if (parsed == null) {
            println("  Unknown command. Type 'help' for commands.")
            continue
        }
        val (n, isAdd) = parsed
        total += if (isAdd) n else -n
        clearScreen()
    }
    println("Final total: $total")
}

main()

Nice for quick CLI tools and one-off automation without a full Kotlin project.



Projects

Site

Games

Tags