DEV Community

Maximiliano Burgos
Maximiliano Burgos

Posted on

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

Bienvenido/a a otro capítulo del Curso de Kotlin! Podés consultar el curso completo desde este link que te dejo acá. Podés seguirme por LinkedIn o Twitter si querés estar al tanto de las próximas publicaciones.

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:

Crear nuevo proyecto

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

Vista package

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
}
Enter fullscreen mode Exit fullscreen mode

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

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

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
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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: 
Enter fullscreen mode Exit fullscreen mode

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?"
)
Enter fullscreen mode Exit fullscreen mode

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!"
        )
    )
)
Enter fullscreen mode Exit fullscreen mode

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:

Debug

¿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

Podemos acceder a una dimensión más:

  • 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!"),
        )
    ),
)
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)