El partido, minuto a minuto
Al terminar: Simular un partido minuto a minuto con una función suspend y delay, y comprobarlo con un test.
En la lección anterior viste qué es suspend y por qué delay no bloquea el hilo. Hoy lo aplicas a Marcador: escribes jugarPartido, una función suspend que juega un partido minuto a minuto, con una pausa por cada minuto. Al terminar, ./gradlew :torneo:run imprimirá el resultado de un partido (2-1) en torno a 1 s de espera real (90 minutos de 10 ms son 900 ms, y cada pausa añade un poco), y tendrás un test que comprueba que el partido jugado reproduce el guion.
Antes del paso, un ejemplo con ollas de cocina (para que veas las piezas nuevas sin los partidos de por medio) y un ejercicio con una receta minuto a minuto. Los dos los puedes probar en el Kotlin Playground.
Piezas nuevas: constante, argumentos y corrutinas lanzadas
Cuatro cosas que aún no se han explicado: launch { … } (arranca una corrutina nueva y sigue sin esperarla; lo verás entero en launch y async), maxOf y maxOfOrNull (el mayor de varios valores), y main(args: Array<String>) con "lento" in args. args es un Array<String> (el Array de tamaño fijo que viste con vararg en object, companion y vararg) con las palabras que se escriben al ejecutar el programa, y in pregunta si una colección contiene algo, como ya viste en Propiedades y visibilidad. También aparece const val, que ya viste en object, companion y vararg. Todo junto en un ejemplo con ollas en vez de partidos, explicado línea a línea:
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
const val PAUSA_MS = 100L // const val: constante conocida al compilar, en mayúsculas
fun main(args: Array<String>) { // args: las palabras escritas tras --args= al ejecutar
if ("lento" in args) println("modo lento") // in: ¿está esa palabra en el array?
runBlocking {
for (i in 0..2) { // i vale 0, 1 y 2
launch { // arranca una corrutina y sigue sin esperarla
delay(PAUSA_MS)
println("Olla $i lista")
}
} // runBlocking espera a que acaben las tres
}
println(maxOf(3, 8)) // el mayor de dos valores: 8
println(listOf(4, 9, 2).maxOfOrNull { it * 10 }) // el mayor de lo que da la lambda: 90
println(emptyList<Int>().maxOfOrNull { it }) // lista vacía: null
}Al ejecutarlo, imprime:
Olla 0 lista
Olla 1 lista
Olla 2 lista
8
90
nullCon eso ya puedes practicar lo que usa el paso, con otros datos.
Reto · Ejercicio: una receta minuto a minuto
En el Kotlin Playground, escribe una función que cocine una receta minuto a minuto. Recibe una lista de pasos, cada uno una pareja con su minuto y su texto, y cuánto dura un minuto en milisegundos (10 si no se dice). Por cada minuto, desde el 1 hasta el del último paso, se pausa sin bloquear el hilo e imprime los pasos de ese minuto. Aunque no haya pasos, la receta dura al menos 5 minutos (una constante del programa). Devuelve cuántos minutos duró. Pruébala con «dar la vuelta» en el minuto 2, «añadir queso» en el 5 y «sacar del horno» en el 7, y después con una lista vacía. La consola debe mostrar:
minuto 2: dar la vuelta
minuto 5: añadir queso
minuto 7: sacar del horno
Pizza lista en 7 minutos
Horno vacío: 5 minutosVer solución
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
const val MINUTOS_MINIMOS = 5
// Cada paso es una pareja (minuto, texto): `first` es el minuto
suspend fun cocinar(pasos: List<Pair<Int, String>>, msPorMinuto: Long = 10): Int {
// El último minuto es el mayor entre el mínimo y el del último paso (0 si no hay pasos)
val ultimo = maxOf(MINUTOS_MINIMOS, pasos.maxOfOrNull { it.first } ?: 0)
for (minuto in 1..ultimo) {
delay(msPorMinuto) // pausa sin bloquear el hilo
pasos.filter { it.first == minuto }.forEach { println("minuto $minuto: ${it.second}") }
}
return ultimo
}
fun main() = runBlocking {
val pizza = listOf(2 to "dar la vuelta", 5 to "añadir queso", 7 to "sacar del horno")
println("Pizza lista en ${cocinar(pizza)} minutos")
println("Horno vacío: ${cocinar(emptyList())} minutos")
}Paso · Un partido que se juega minuto a minuto
Objetivo. Apartar el main de antes, crear jugarPartido (suspendida, con un delay por minuto), traer los datos de ejemplo y escribir un main nuevo. Al terminar, ./gradlew :torneo:run imprime el resultado de un partido.
Primero, el main de antes se aparta a su propio fichero, para dejar Main.kt libre para la lección nueva:
-
En la vista Android, en
torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo, clic derecho sobreMain.kt→ Refactor → Rename… y escribeDemoKotlin. Si el IDE pregunta qué más renombrar, deja marcado solo el fichero y desmarca «Search in comments and strings» y «Search for text occurrences» (buscar en comentarios y cadenas de texto, y buscar apariciones del nombre como texto), para que no toque nada más. Una comprobación: abreGradle Scripts → build.gradle.kts (Module :torneo)y mira que la líneamainClass.set(...)sigue terminando enMainKt. No la cambies: apunta alMain.ktnuevo que vas a crear dentro de un momento. Marcador se ejecuta con./gradlewdesde la Terminal, no con el botón verde de Android Studio. -
Abre
DemoKotlin.kt. Cambiafun main()porfun demoKotlin()(sin parámetros). Borra el comentario que hay encima defun demoKotlin()(// Punto de entrada de consola: ./gradlew :torneo:run) y pon en su lugar estos dos, debajo de losimport:DemoKotlin.kt: los dos comentarios nuevos // La demo del módulo de Kotlin, sin corrutinas. Ya no es el punto de entrada: // se lanza con ./gradlew :torneo:run --args=kotlinLo que hace la segunda línea se ve más abajo, en el
mainnuevo: si pasas la palabrakotlinal ejecutar, el programa lanza esta demo.
Después, los ficheros nuevos. Todos se crean igual: clic derecho sobre el paquete (com.sebastiannarvaez.marcador.torneo para el código, com.sebastiannarvaez.marcador.torneo (test) para el test) → New → Kotlin Class/File → File → el nombre.
Ejemplo.kt se rellena con el botón. Son unos datos de prueba (40 líneas de jugadores y eventos) que no enseñan nada de corrutinas, así que no los escribes. El botón Copiar fichero funciona como se explicó en Clases y objetos: copia el contenido al portapapeles y no escribe nada en tu proyecto. El orden es siempre el mismo: crea el fichero Ejemplo.kt en el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test)), pulsa el botón, haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
Dentro hay un object Ejemplo con equipos y dos partidos. Es lo que usarás:
val rayoContraToros = Partido(
local = rayo,
visitante = toros,
eventos = listOf(
Gol(12, ana, rayo), // minuto 12
Tarjeta(30, ivan, ColorDeTarjeta.AMARILLA),
Gol(55, ivan, toros),
Gol(80, ana, rayo),
Cambio(85, sale = ana, entra = luis),
),
)Intenta escribirlo tú, en tres ficheros (usa lo de las ollas y la receta de arriba):
Simulacion.kt.jugarPartido(partido, msPorMinuto)recibe el guion (todo lo que va a pasar) y lo juega: parte de un partido vacío con los mismos equipos, se pausa sin bloquearmsPorMinutomilisegundos (10 si no se dice) por cada minuto del 1 al 90 y va registrando los eventos de cada minuto. Los eventos del minuto 0 entran antes de empezar, y si el guion tiene alguno después del 90 (tiempo añadido), el partido dura hasta ese minuto. Devuelve el partido jugado. Los 90 minutos son una constante del fichero.Main.kt. Si al ejecutar se pasa la palabrakotlin, lanza la demo de antes y termina. Si no, juega el Rayo FC-Toros deEjemploy muestra el resultado.SimulacionTest.kt. Tres tests, con los minutos a 0 ms para que no esperen: el partido jugado reproduce el guion (los mismos eventos y 2-1), jugarlo no modifica el guion (sigue con 5 eventos) y un partido sin eventos termina sin goles.
Al ejecutar ./gradlew :torneo:run en la Terminal de Android Studio debe verse:
== un partido suspendido ==
Final: 2-1 con 5 eventosEl Main.kt de abajo lo escribes tú: no lleva la demo de las 10.000 corrutinas, que ya probaste en el Playground.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Simulacion.kt:
package com.sebastiannarvaez.marcador.torneo
import kotlinx.coroutines.delay
const val MINUTOS_DEL_PARTIDO = 90
// `suspend` NO significa «corre en otro hilo». Significa «esta función puede
// pausarse»: dentro puede llamar a otras `suspend` (como `delay`) y, mientras
// espera, el hilo queda libre para hacer otra cosa. Solo se puede llamar desde
// otra `suspend` o desde una corrutina.
//
// `partido` es el GUION: todo lo que va a pasar. La función lo «juega» minuto a
// minuto y devuelve el partido tal como quedó. `msPorMinuto` es cuánto dura un
// minuto de juego en tiempo real (10 ms hace un partido de menos de un segundo).
suspend fun jugarPartido(partido: Partido, msPorMinuto: Long = 10): Partido {
var jugado = Partido(partido.local, partido.visitante)
// Lo que pasa «antes de empezar» (minuto 0) no necesita esperar.
partido.eventos.filter { it.minuto == 0 }.forEach { jugado = jugado.registrar(it) }
val ultimoMinuto = maxOf(MINUTOS_DEL_PARTIDO, partido.eventos.maxOfOrNull { it.minuto } ?: 0)
for (minuto in 1..ultimoMinuto) {
// `delay` suspende SIN bloquear el hilo. `Thread.sleep` lo bloquearía.
delay(msPorMinuto)
partido.eventos.filter { it.minuto == minuto }.forEach { jugado = jugado.registrar(it) }
}
return jugado
}Una duda razonable sobre ultimoMinuto: un partido dura 90 minutos, ¿por qué no recorrer siempre 1..90? Porque un evento del guion podría estar anotado en el minuto 93 (el tiempo añadido por el árbitro); con maxOf el bucle llega hasta ahí y ese evento se registra en lugar de perderse.
Main.kt, el nuevo:
package com.sebastiannarvaez.marcador.torneo
import kotlinx.coroutines.runBlocking
// Punto de entrada de consola: ./gradlew :torneo:run
// (la demo del módulo de Kotlin sigue disponible: ./gradlew :torneo:run --args=kotlin)
//
// `runBlocking` es el puente entre el mundo normal (`main` no es suspend) y el
// de las corrutinas: bloquea el hilo actual hasta que el bloque termina.
fun main(args: Array<String>) {
if ("kotlin" in args) {
demoKotlin()
return
}
runBlocking {
println("== un partido suspendido ==")
val final = jugarPartido(Ejemplo.rayoContraToros)
println("Final: ${final.golesLocal}-${final.golesVisitante} con ${final.eventos.size} eventos")
}
}Dos detalles. Primero, return sale de main en ese momento, sin ejecutar nada más: por eso, con la palabra kotlin, solo se lanza la demo. Segundo, args son las palabras que escribes tras --args= al ejecutar.
runBlocking también sirve en los tests: lo usa este, con msPorMinuto = 0 para que delay(0) vuelva enseguida. Se crea en el paquete com.sebastiannarvaez.marcador.torneo (test), igual que los otros.
package com.sebastiannarvaez.marcador.torneo
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
class SimulacionTest {
// `= runBlocking { … }` es un cuerpo de expresión (como en Funciones): la función
// devuelve lo que da el bloque. `runBlocking` permite probar código suspendido sin más librerías. Más adelante
// verás `runTest`, que además adelanta el reloj en vez de esperar de verdad.
@Test
fun jugarUnPartidoReproduceElGuion() = runBlocking {
val jugado = jugarPartido(Ejemplo.rayoContraToros, msPorMinuto = 0)
assertEquals(Ejemplo.rayoContraToros.eventos, jugado.eventos)
assertEquals(2, jugado.golesLocal)
assertEquals(1, jugado.golesVisitante)
}
@Test
fun elPartidoNoModificaElGuion() = runBlocking {
val guion = Ejemplo.rayoContraToros
jugarPartido(guion, msPorMinuto = 0)
assertEquals(5, guion.eventos.size)
}
@Test
fun unPartidoSinEventosTerminaSinGoles() = runBlocking {
val jugado = jugarPartido(Partido(Ejemplo.rayo, Ejemplo.toros), msPorMinuto = 0)
assertEquals(0, jugado.eventos.size)
}
}Punto de control
- En
torneo → kotlin+java → com.sebastiannarvaez.marcador.torneose venDemoKotlin.kt,Main.kt,Simulacion.ktyEjemplo.kt. - En la Terminal de Android Studio,
./gradlew :torneo:runtermina enBUILD SUCCESSFULy imprime:
== un partido suspendido ==
Final: 2-1 con 5 eventos./gradlew :torneo:run --args=kotlinlanza la demo de antes, la del módulo de Kotlin sin corrutinas../gradlew :torneo:testtermina enBUILD SUCCESSFUL. Abre el informe (open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test) y comprueba que arriba pone 85 tests (los 82 de antes más los 3 deSimulacionTest), y que en el paquete(test)estáSimulacionTest.kt.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.
jugarPartidocalculaultimoMinutoconmaxOf(MINUTOS_DEL_PARTIDO, …)en lugar de recorrer siempre1..90. ¿Por qué?2.¿Qué hace
./gradlew :torneo:run --args=kotlinen Marcador?3.Los tests de
SimulacionTestllaman ajugarPartido(…, msPorMinuto = 0). ¿Para qué?4.En
listOf(4, 9, 2).maxOfOrNull { it * 10 }y enemptyList<Int>().maxOfOrNull { it }, ¿qué devuelven?5.Dentro de
runBlocking,for (i in 0..2) { launch { delay(100); println("Olla $i lista") } }tarda unos 100 ms en total, no 300. ¿Por qué?