Go Fish/Kotlin

From Rosetta Code
// version 1.1.51

import java.util.Random

const val FACES = "23456789tjqka"
const val SUITS = "cdhs"

enum class Turn { user, comp }

class Card(val face: Char, val suit: Char) : Comparable<Card> {
    private val value = FACES.indexOf(face) * 4 + SUITS.indexOf(suit)

    override fun compareTo(other: Card) = this.value.compareTo(other.value)

    override fun toString() = "$face$suit"
}

val r = Random()

val ranks = listOf(
    "twos", "threes", "fours", "fives", "sixes", "sevens", "eights",
    "nines", "tens", "jacks", "queens", "kings", "aces"
)

val deck = mutableListOf<Card>()

val userHand  = mutableListOf<Card>()
val userBooks = mutableListOf<Char>()
val compHand  = mutableListOf<Card>()
val compBooks = mutableListOf<Char>()
val compPrev  = mutableListOf<Char>()

var turn = Turn.user // user always starts

fun createDeck() {
    for (suit in SUITS) {
        for (face in FACES) deck.add(Card(face, suit))
    }
}

fun shuffleDeck() {
    val shuffled = mutableListOf<Card>()
    do {
        val card = deck[r.nextInt(52)]
        if (card !in shuffled) shuffled.add(card)
    } while (shuffled.size < 52)
    deck.clear()
    deck.addAll(shuffled)
}

/* Chooses a rank at random provided it hasn't already been chosen
   during the current turn. If all ranks have already been chosen,
   it chooses the first again. */
fun compSelectRankIndex(): Int {
    val choices = compHand.map { it.face }.distinct().filter { it !in compPrev }
    val size = choices.size
    val choice = if (size == 0) compHand[0].face else choices[r.nextInt(size)]
    return FACES.indexOf(choice)
}

val userChoices get() = userHand.map { it.face }.distinct()

fun printHand() = println("Your cards : ${userHand.joinToString("  ")}")

fun printBooks() {
    println("Your books : ${userBooks.joinToString("   ")}")
    println("My books   : ${compBooks.joinToString("   ")}")
}

fun printTurn() =
    println(if (turn == Turn.user) "--- YOUR TURN ---" else "--- MY TURN ---")

fun checkForBooks(hand: MutableList<Card>, books: MutableList<Char>) {
    val newBooks = hand.groupBy { it.face }
                       .filter { it.value.size == 4 }
                       .map { it.key }
    if (newBooks.size > 0) {
        books.addAll(newBooks)
        books.sort()
        for (b in newBooks) {
            for (i in hand.size - 1 downTo 0) {
                if (hand[i].face == b) hand.removeAt(i)
            }
        }
        if (hand.size == 0 && deck.size > 0) {
            val e = deck.removeAt(0)
            if (hand === userHand) println("You drew : $e")
            hand.add(e)
        }
        println("Added ${newBooks.joinToString("   ")} to books")
    }
}

fun showStateOfPlay(showTurn: Boolean = true) {
    println()
    userHand.sort()
    printHand()
    printBooks()
    if (showTurn) printTurn()
    println()
}

fun main(args: Array<String>) {
    // create and shuffle deck and deal cards
    createDeck()
    shuffleDeck()
    for (i in 0..16 step 2) userHand.add(deck[i])
    for (i in 1..17 step 2) compHand.add(deck[i])
    for (i in 0..17) deck.removeAt(0)

    // check if there are any books in initial hands
    checkForBooks(userHand, userBooks)
    checkForBooks(compHand, compBooks)
    showStateOfPlay()

    while (true) {
        while (true) {
            var rank: String

            if (turn == Turn.user) {
                val choices = userChoices
                while (true) {
                    print("Enter the rank you want : ")
                    rank = readLine()!!.toLowerCase()
                    if (rank !in ranks) continue
                    val choice = FACES[ranks.indexOf(rank)]
                    if (choice in choices) break
                }
            }
            else {
                val r = compSelectRankIndex()
                rank = ranks[r]
                println("The rank I want is : $rank")
                compPrev.add(FACES[r])
            }
            val face = FACES[ranks.indexOf(rank)]
            var matches = 0
            if (turn == Turn.user) {
                for (i in compHand.size - 1 downTo 0) {
                    if (compHand[i].face == face) {
                        matches++
                        userHand.add(compHand.removeAt(i))
                    }
                }
                println("Matches : $matches")
                if ((matches == 0 || userHand.size == 0) && deck.size > 0) {
                    val e = deck.removeAt(0)
                    println("You drew : $e")
                    userHand.add(e)
                }
                checkForBooks(userHand, userBooks)
                if (userBooks.size >= 7) {
                    showStateOfPlay(false)
                    println("Congratulations, you've won!")
                    return
                }
            }
            else {
                for (i in userHand.size - 1 downTo 0) {
                    if (userHand[i].face == face) {
                        matches++
                        compHand.add(userHand.removeAt(i))
                    }
                }
                println("Matches: $matches")
                if ((matches == 0 || compHand.size == 0) && deck.size > 0) {
                    val e = deck.removeAt(0)
                    compHand.add(e)
                }
                checkForBooks(compHand, compBooks)
                if (compBooks.size >= 7) {
                    showStateOfPlay(false)
                    println("Commiserations, but I've won!")
                    return
                }
            }
            if (matches > 0) showStateOfPlay() else break
        }
        turn = if (turn == Turn.user || userHand.size == 0) Turn.comp else Turn.user
        if (turn == Turn.comp) compPrev.clear()
        showStateOfPlay()
    }
}