Curso Kotlin | #18. Vamos a darle vida al heroe

Luego de la explicación teórica que di en el artículo anterior sobre como va a ser este juego, The Hero Legacy, hoy finalmente vamos a escribir código hasta que se nos caigan las manos o las ideas. Bienvenidos/as, nuevamente, al inicio del primer proyecto del curso de Kotlin: El legado del Heroe.

Manos a la obra

Para evitar problemas a futuro, dejaremos nuestro proyecto de ejemplos de Kotlin Console Tutorial para explicar temas puntuales; y crearemos un nuevo proyecto llamado The Hero Legacy en Intellij:

Una vez tenemos el proyecto creado, recomiendo utilizar la vista Package para mayor comodidad:

Vamos a crear los atributos que tendrá nuestro heroe en forma de variables: nombre (name), puntos de vida (hp) y monedas (coins) de momento:

fun main(args: Array<String>) {
    var heroName: String
    var heroHp: Int = 100
    var heroCoins: Int = 0
}

El nombre de nuestro heroe lo determinaremos en base al input de la consola:

println("Buenas! Cual es tu nombre?")
heroName = readLine().toString()

Vamos a introducir este comportamiento en una funcion para empezar a separar los términos; también le sumaremos algunas validaciones:

fun getHeroName(): String {
    println("Cual es tu nombre?")
    var name = readLine()

    while (name.isNullOrEmpty()){
        println("No seas timid@! Dime tu nombre...")
        name = readLine()
    }

    println("Hola $name!")

    return name
}

Como puedes observar, he creado la condición de que si el nombre es nulo o vacío, no deje de preguntarlo nuevamente hasta que eso sea falso. Es una buena manera de crear una validación que puede iterar dentro de un flujo constante.

Ahora llamaremos esta función dentro de main:

val heroName = getHeroName()

Convertí heroName en una variable inmutable porque ya no necesitamos cambiarla en tiempo de ejecución, sino que solo se le asignará en el momento de llamar a la función getHeroName.

Diálogos

Ahora que nuestro heroe tiene un nombre, vamos a lanzarlo al mundo a charlar con NPCs:

fun letsTalk() {
    println("Bienvenido a Pueblo Ceniza! Eres nuev@ por aqui?")
    println("1. Si, soy nuev@")
    println("2. No es mi primer visita!")

    try {
        when(readLine()?.toInt()){
            1 -> println("Espero que puedas hacer nuevos amigos!")
            2 -> println("Ya me parecía que tu nombre me resultara conocido!")
            else -> println("Disculpa, no te entendí...")
        }
    } catch (e: Exception) {
        println("No has respondido mi pregunta...")
        letsTalk()
    }
}

Primero le damos la bienvenida al heroe y le preguntamos si es nuevo en el pueblo. Necesitamos que elija una opción numérica, por lo cual casteamos el input a entero; pero lo envolvemos dentro de un try catch por si introduce caracteres no numéricos. Si elige una opción numérica distinta a las planteadas, vamos por el else. Pero si cae en el catch, usamos una estrategia muy conocida en el mundo del desarrollo llamado “función recursiva”.

Funciones Recursivas

Un método o función recursivo es aquel que se puede llamar a si mismo cuando lo requiera. Es algo muy utilizado cuando vemos conceptos más abstractos como la teoría de los árboles binarios. Es una estrategia muy interesante cuando se aplica con parámetros, porque los mismos se van pasando entre la recursividad:

fun tellMeSomething(something: String) {
    if(something.isNotEmpty()) println("Estoy de acuerdo con $something")
    print("Dime algo: ")
    val words = readLine().toString()
    tellMeSomething("$something $words")
}

Esta función se llamará eternamente, pero si lanzamos un par de respuestas, quedará asi:

Dime algo: manzanas
Estoy de acuerdo con  manzanas
Dime algo: peladas
Estoy de acuerdo con  manzanas peladas
Dime algo: tomates
Estoy de acuerdo con  manzanas peladas tomates
Dime algo: 

Escalabilidad de los diálogos

Volviendo al diálogo que armamos, podemos decir que hemos terminado el trabajo. Pero tenemos un serio problema de escalabilidad: si queremos agregar más diálogos, esto se volverá un suplicio. Por lo cual necesitamos la ayuda de nuestros queridos arreglos. En principio vamos a migrar nuestra conversación a un array:

val talk = arrayOf(
    "Bienvenido a Pueblo Ceniza! Eres nuev@ por aqui?"
)

Ahora necesitamos guardar las opciones. Podríamos crear otro array, pero de nuevo perderíamos escalabilidad. Por lo tanto, propongo lo siguiente:

val conversation = arrayOf(
    arrayOf(
        "Bienvenido a Pueblo Ceniza! Eres nuev@ por aqui?",
        arrayOf(
            "1. Si, soy nuev@",
            "2. No es mi primer visita!"
        )
    )
)

No te asustes: se trata de un array multidimensional; en pocas palabras, un array dentro de otro, y de otro. Esto quedará más claro si mostramos esta variable en la inspección del debugger:

¿Qué utilidad nos brinda esto? Podemos saber en que dimensiones del array va a estar cada elemento:

  • talk[0] contendrá nuestra linea de diálogo
  • talk[0][0] será la pregunta
  • talk[0][1] será el conjunto de respuestas
    • talk[0][1][0] la primer respuesta
    • talk[0][1][1] la segunda

El problema es que si ahora elegimos una respuesta, nuestro NPC no continuará el diálogo, por lo cual necesitamos modificar un poco más el comportamiento de este array:

val conversation = arrayOf(
    arrayOf(
        "Bienvenido a Pueblo Ceniza! Eres nuev@ por aqui?",
        arrayOf(
            arrayOf(1, "1. Si, soy nuev@"),
            arrayOf(2, "2. No es mi primer visita!")
        )
    ),
    arrayOf(
        "Espero que puedas hacer nuevos amigos!",
        arrayOf(
            arrayOf(0, "1. Gracias!"),
        )
    ),
    arrayOf(
        "Ya me parecía que tu nombre me resultara conocido!",
        arrayOf(
            arrayOf(0, "1. Gracias!"),
        )
    ),
)

A cada respuesta posible le agregamos una primera posición del indice donde tendrá que ir a buscar la respuesta. Aquellas que contienen el indice cero vuelven a la pregunta inicial, sería como un reinicio del diálogo. Al usar un arreglo, tenemos escalabilidad hacia donde lo necesitemos, tanto en cantidad de diálogos como preguntas y posibles respuestas. Ahora vamos a aplicarlo a nuestro código por medio de la iteración:

fun letsTalk(line: Int) {
    val answer = conversation[line][0]
    println(answer)

    val responses = conversation[line][1] as Array<*>
    for(res in responses) {
        println(res[1]) // problema :(
    }

    try {
        letsTalk(readLine()?.toInt()!!)
    } catch (e: Exception) {
        println("No has respondido mi pregunta...")
        letsTalk(line)
    }
}

En principio imprimo la pregunta tomando la primer posición, porque en la invocación (implícita) llamé a la función como letsTalk(0). Luego guardo las respuestas y las casteo a un Array con el asterisco (*), lo cual implica que van a haber muchos tipos dentro del mismo. Itero entre las posibles respuestas, y finalmente envio el input como parámetro de nuestra llamada recursiva de la función.

El problema, como indico en los comentarios del código, es que no podemos acceder de esa manera a los indices de la variable response. A esta altura, estaríamos mirando la siguiente parte del array:

Iteración 1: arrayOf(1, "1. Si, soy nuev@")
Iteración 2: arrayOf(2, "2. No es mi primer visita!")

Esto nos genera una complejidad adicional, pero podemos suplirla con un concepto que veremos en la siguiente clase.

Conclusiones

Puede que este proyecto te resulte algo complejo, pero no te preocupes: hay muchas cosas que se reescribirán de un modo mucho más simple cuando trabajemos con objetos. Además, depender de un gran array puede ser tedioso, por lo que en el futuro trabajaremos con JSON. Todavía queda un largo camino, pero aprender a hacer las cosas con pocos recursos y conocimientos, nos permitirá entender mejor los próximos tópicos.

¡Deja un comentario!

Artículos relacionados

Curso Kotlin | #19. Pair

A veces necesitamos relacionar dos valores y almacenarlos en una variable única, y para ello tenemos a Pair.

Curso Kotlin | #17. Proyecto: The Hero Legacy

Si empezaste esta serie desde cero y ya leíste 16 capítulos, ¡Felicidades! Empezaste a dominar los primeros conceptos fundamentales en Kotlin. Este es el primer paso de tu senda como desarrollador/a. Estamos en una instancia en donde podemos poner en práctica todo lo que aprendiste hasta ahora. Bienvenido/a al primer proyecto de la serie.

Curso Kotlin | #16. Funciones

Las funciones son procedimientos que se pueden reutilizar y nos permiten encapsular comportamientos y mejorar la lectura del código.

Curso Kotlin | #15. Excepciones

Las excepciones en Kotlin nos permiten evitar comportamientos no deseados ante problemas que se presentan en tiempo de ejecución. Por ejemplo cuando se intenta dividir por cero, castear un tipo por otro no válido, intentar agregar un valor a una lista nula, entre otros casos. Hoy vamos a ver su implementación en el código.

Curso Kotlin | #14. Rangos

Cuando hablamos de rangos, nos referimos a un intervalo de números en el sentido matemático. Podemos generar, por ejemplo, un rango que comprenda los números del 1 al 10. Hoy veremos como aplicarlo a los conceptos anteriormente aprendidos.

Curso Kotlin | #13. Bucle For

El bucle For es uno de los más utilizados por todos los lenguajes de programación. Nos permite recorrer arrays y listas, como también generar iteraciones con un límite contabilizado. Vamos a verlo en detalle.