Maximiliano Burgos
Posted on December 15, 2022
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:
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
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!"),
)
),
)
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.
Posted on December 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.