Saltar a contenido

Trabajo Práctico Final: Guitar Hero#

Nota

Es posible proponer otro proyecto como trabajo final. En ese caso, se debe escribir una consigna de estilo similar a esta y presentarla a los docentes de la práctica. Ellos determinarán si la propuesta es o no viable. Procuren decidir esto a la brevedad, para maximizar el tiempo disponible para el desarrollo del trabajo.

Introducción#

En 2005 salió a la venta Guitar Hero, un videojuego que se juega con una guitarra de plástico de cinco botones de colores. Mientras suena una canción, por la pantalla bajan notas de esos mismos colores, y el jugador tiene que apretar el botón correcto justo cuando cada nota llega al final de la pantalla. Fue un fenómeno mundial: la saga vendió más de 25 millones de copias y llevó a toda una generación a escuchar a Queen, Metallica y los Rolling Stones.

El control y la pantalla del primer Guitar Hero
El control con forma de guitarra y la pantalla del primer Guitar Hero (2005). Fuentes: foto del control (dominio público) y captura del juego (Wikipedia)

Detrás de cada canción había un trabajo enorme y completamente manual. Un equipo de diseñadores escuchaba el tema una y otra vez y escribía a mano cada nota: en qué momento se toca y con qué botón. Y no lo hacían una vez sino cuatro, una por cada nivel de dificultad (fácil, medio, difícil y experto). Para no empezar de cero cada vez, primero escribían el nivel experto y después lo iban simplificando con reglas que respetaban siempre: sacar las notas más rápidas, juntar botones, eliminar los acordes. Por eso el juego solo tenía las canciones que alguien se había sentado a escribir.

Hoy existe otra forma: una red neuronal, un programa que aprende a hacer una tarea viendo muchísimos ejemplos, en lugar de seguir instrucciones escritas por una persona. La cátedra les entrega una red neuronal que aprendió viendo miles de niveles hechos a mano: le dan cualquier canción y escribe su nivel experto. En este trabajo, ustedes van a programar las reglas de los diseñadores para armar las otras tres dificultades a partir de ese nivel experto, y el juego para jugarlas.

Desarrollo#

Se debe implementar en Python un juego al estilo Guitar Hero utilizando la librería PyGame. El juego toma canciones en formato WAV, usa la red neuronal de la cátedra para obtener el nivel experto de cada una, lo guarda en un archivo de texto y arma a partir de él los niveles fácil, medio y difícil. Se pueden usar PyGame, la librería estándar de Python y numpy.

Algunas palabras que se usan en toda la consigna:

Palabra Qué significa
Beat El pulso de la canción: los golpes que uno marca con el pie o con la cabeza al escucharla. La velocidad de una canción se mide en beats por minuto (bpm): una canción a 120 bpm tiene un beat cada medio segundo.
Carril Cada una de las columnas por las que bajan las notas. Hay 5 carriles, numerados de 0 a 4 de izquierda a derecha: verde, rojo, amarillo, azul y naranja. Cada carril se toca con una tecla.
Nota Una indicación de que hay que apretar la tecla de un carril en un instante dado.
Acorde Dos o más notas en el mismo instante, en carriles distintos: hay que apretar esas teclas a la vez.
Nota larga Una nota que, además de apretarse en su instante, tiene que mantenerse apretada durante un tiempo (su duración). Las demás son notas cortas.
Golpe Todas las notas que están en el mismo instante. Un golpe puede tener una sola nota o varias (un acorde).

La red neuronal#

La red neuronal viene en un módulo de Python llamado autochart: una carpeta con código que se usa con import autochart, igual que math o random. Ustedes no tienen que entender ni modificar lo que hay adentro de esa carpeta: solo llamar a sus funciones, como se explica a continuación.

Instalación#

  1. Descargar autochart.zip (ocupa unos \(450\) MB; para descargarlo hay que estar conectado a Google Drive con la cuenta de la universidad) y descomprimirlo en la carpeta del proyecto, de modo que quede una carpeta autochart/ al lado de su código. Agregar la línea autochart/modelos/ al archivo .gitignore del repositorio (esos archivos son demasiado grandes para GitHub).
  2. Instalar las librerías que usa la red (son varios GB):

    pip install torch torchaudio demucs beat_this
    
  3. Instalar ffmpeg, el programa que usa la red para leer el audio:

    • Linux: sudo apt install ffmpeg
    • macOS: brew install ffmpeg
    • Windows: winget install ffmpeg (y después cerrar y volver a abrir la terminal)

Para verificar que todo quedó instalado, ejecutar en Python:

import autochart
print(autochart.disponible())

Tiene que mostrar True. Si muestra False, falta alguno de los pasos anteriores.

Instalen la red la primera semana

La instalación es pesada y puede dar problemas según la computadora. Háganla cuanto antes y, si algo falla, consulten a los docentes. Mientras tanto pueden avanzar con los archivos de nivel de ejemplo que se describen en Archivo de nivel.

Cómo usarla#

La función autochart.generar(ruta) recibe la ruta de un archivo WAV y devuelve un resultado con el nivel experto de esa canción:

import autochart

resultado = autochart.generar("canciones/Rock nacional/de_musica_ligera.wav")

print(resultado.beats[:6])                  # los primeros 6 beats
for nota in resultado.notas["experto"][:6]:     # las primeras 6 notas
    print(nota)

Para “De música ligera” (Soda Stereo), este programa muestra:

(0.0, 0.28, 0.76, 1.243, 1.7226, 2.2022)
Nota(tiempo=0.76, trastes=(2, 4), duracion=0.3622)
Nota(tiempo=1.243, trastes=(2, 4), duracion=0.2398)
Nota(tiempo=1.6027, trastes=(0,), duracion=0.0)
Nota(tiempo=1.7226, trastes=(1,), duracion=0.0)
Nota(tiempo=1.9624, trastes=(1,), duracion=0.0)
Nota(tiempo=2.2022, trastes=(1, 3), duracion=0.362)

La primera línea son los instantes de los primeros \(6\) beats, en segundos. Después vienen las notas: la primera es un acorde amarillo y naranja (carriles \(2\) y \(4\)) en \(0.76\) s, que es una nota larga de \(0.3622\) s; la tercera es una nota corta verde (carril \(0\)) en \(1.6027\) s; y la última es un acorde rojo y azul (carriles \(1\) y \(3\)) en \(2.2022\) s.

Las primeras notas de De música ligera
Las mismas 6 notas dibujadas: cada fila es un carril, las líneas verticales son los beats y las barras son notas largas

El resultado tiene dos atributos:

Atributo Qué es
resultado.beats Una tupla con el instante, en segundos desde el comienzo de la canción, de cada beat. Está ordenada de menor a mayor.
resultado.notas["experto"] Una lista con las notas del nivel experto, ordenadas por tiempo.

Cada nota de esa lista tiene estos atributos:

Atributo Qué es
tiempo El instante de la nota, en segundos desde el comienzo de la canción (un float).
trastes Una tupla con los carriles de la nota (enteros de \(0\) a \(4\)). Si tiene más de un carril, es un acorde. Por ejemplo, (1, 3) es un acorde rojo y azul.
duracion La duración en segundos, si es una nota larga, o 0 si es una nota corta.

La red siempre da el mismo resultado para la misma canción. generar no guarda nada en ningún archivo: devuelve el resultado y nada más.

Tiempo de generación. La red tarda en generar un nivel menos de la mitad de lo que dura la canción (en una notebook con un procesador Apple M2, alrededor de un minuto y medio para una canción de \(3\) minutos y medio). En computadoras más viejas puede tardar más, y en computadoras con placa de video NVIDIA, mucho menos. Por eso el nivel generado se guarda en un archivo, para generarlo una sola vez por canción.

Mostrar el avance. Como la generación tarda, generar puede recibir una función, escrita por ustedes, a la que va llamando mientras trabaja para avisar cuánto avanzó. Esa función tiene que recibir dos parámetros: la fracción completada (un número entre \(0\) y \(1\)) y un texto que dice qué está haciendo la red. Se la pasa con el parámetro progreso, escribiendo solo su nombre, sin paréntesis:

def mostrar_avance(fraccion, texto):
    print(f"{fraccion * 100:.0f}% - {texto}")

resultado = autochart.generar("canciones/Rock/believer.wav", progreso=mostrar_avance)

Este ejemplo muestra en la terminal líneas como 47% - Separando los instrumentos.

Cancelar. Si la función de progreso lanza una excepción, generar se interrumpe y esa misma excepción sale de generar, donde se la puede atrapar con try/except. Así se puede cancelar una generación.

Errores. Si el archivo no existe, generar lanza FileNotFoundError. Si falla por cualquier otro motivo (por ejemplo, un archivo WAV dañado), lanza otra excepción. En todos los casos, el juego debe atrapar la excepción, mostrar el mensaje en pantalla y volver al menú, sin cerrarse.

Para curiosos: ¿cómo funciona la red por dentro?

autochart está basado en Autochart, un generador de niveles para Clone Hero y YARG. En realidad usa tres redes neuronales. La primera (Demucs) separa la canción en batería, bajo, voz y el resto de los instrumentos. La segunda (Beat This) encuentra los beats. La tercera (Fretformer) “escucha” la canción beat por beat y escribe el nivel como si fuera un texto, símbolo por símbolo, eligiendo en cada paso el símbolo más probable. Fretformer es del mismo tipo que las redes que usan los asistentes como ChatGPT para escribir texto.

Del resultado de la red a las notas del juego#

En el juego, una nota es una tupla (tiempo, carril, duracion): el instante en segundos, el carril (de \(0\) a \(4\)) y la duración en segundos (0 si es una nota corta). Un acorde se representa con varias tuplas con el mismo tiempo, una por carril. Para pasar las notas de la red a este formato:

  1. Redondear el tiempo de cada nota de la red a \(3\) decimales (al milisegundo).

    Ejemplo

    Nota(tiempo=1.6027, trastes=(0,), duracion=0.0) pasa a tener tiempo 1.603.

  2. Separar los acordes: cada carril de trastes da una tupla (tiempo, carril, duracion).

    Ejemplo

    Nota(tiempo=0.76, trastes=(2, 4), duracion=0.3622) da dos tuplas: (0.76, 2, 0.3622) y (0.76, 4, 0.3622).

  3. Eliminar repetidas: la red a veces repite la misma nota. Si hay varias tuplas con el mismo tiempo y el mismo carril, queda una sola, con la mayor de sus duraciones.

    Ejemplo

    (1.243, 2, 0) y (1.243, 2, 0.2398) quedan como una sola tupla, (1.243, 2, 0.2398).

  4. Recortar notas largas: una nota larga tiene que terminar al menos \(0.05\) s antes de la siguiente nota de su mismo carril. Si termina después, se le baja la duración a (tiempo de la siguiente nota de ese carril − \(0.05\) − su tiempo). Después, toda nota con duración menor que \(0.1\) s pasa a tener duración 0 (nota corta), y las demás duraciones se redondean a \(3\) decimales.

    Ejemplos

    • (1.243, 4, 0.2398) terminaría en \(1.4828\) s, pero la siguiente nota del carril \(4\) está en \(1.4\) s. Se le baja la duración a \(1.4 - 0.05 - 1.243 = 0.107\): queda (1.243, 4, 0.107).
    • (2.0, 1, 0.3), si la siguiente nota del carril \(1\) está en \(2.1\) s, se baja a \(2.1 - 0.05 - 2.0 = 0.05\). Como es menor que \(0.1\), pasa a ser una nota corta: (2.0, 1, 0).
    • (0.76, 2, 0.3622), si no hay ninguna nota del carril \(2\) antes de \(1.1722\) s, no se recorta, pero su duración se redondea: (0.76, 2, 0.362).
  5. Ordenar las tuplas por tiempo y, a igual tiempo, por carril.

    Ejemplo

    [(1.243, 4, 0), (0.76, 2, 0), (0.76, 1, 0)] queda [(0.76, 1, 0), (0.76, 2, 0), (1.243, 4, 0)].

Estas son las notas del nivel experto.

Archivo de nivel#

El nivel experto de cada canción se guarda en un archivo de nivel: un archivo de texto (en Guitar Hero se lo llama chart). Las otras dificultades no se guardan: se arman a partir del experto (ver Dificultades) cada vez que se elige jugarlas.

  • El archivo de nivel de la canción canciones/<género>/<nombre>.wav se llama <nombre>.txt y está en la carpeta charts/. Por ejemplo, el de canciones/Rock/believer.wav es charts/believer.txt. Por eso no puede haber dos canciones con el mismo nombre de archivo, aunque estén en géneros distintos.
  • Si al elegir una canción su archivo de nivel existe, se lo carga. Si no existe, se genera el nivel con la red y se lo guarda antes de jugar.
  • Las otras dificultades se arman siempre con las notas y los beats tal como quedaron en el archivo de nivel (con \(3\) decimales), aunque la canción se acabe de generar. Así, una canción da siempre los mismos niveles.

Un archivo de nivel se ve así:

# cancion: believer.wav
# beats: 0.512 0.981 1.450 1.919 2.388 2.857
0.512 1
0.512 2
0.981 0
1.450 2 0.750
2.388 4

Cada línea del archivo es de uno de estos tipos:

Tipo de línea Formato Ejemplo
Encabezado # clave: valor # cancion: believer.wav
Nota corta tiempo carril 0.981 0
Nota larga tiempo carril duracion 1.450 2 0.750

Reglas del formato:

  • Encabezado. Las líneas de encabezado van todas al principio del archivo, antes de cualquier nota. Empiezan con #, y la clave está separada del valor por :. Hay dos claves obligatorias, cancion y beats, y puede haber otras, que el juego no está obligado a usar.
  • Clave cancion: el nombre del archivo WAV de la canción (sin la carpeta).
  • Clave beats: los instantes de los beats de la canción (resultado.beats), en segundos, separados por un espacio y, al guardar, con exactamente \(3\) decimales. Tiene que haber al menos dos, ninguno negativo, y cada uno mayor que el anterior.
  • Notas. Cada nota ocupa una línea, con \(2\) o \(3\) números separados por un espacio: el tiempo (en segundos), el carril (un entero de \(0\) a \(4\)) y, solo si es una nota larga, la duración (en segundos). En los números con decimales se usa el punto, y al guardar se escriben con exactamente \(3\) decimales. En el ejemplo, en \(0.512\) s hay un acorde (carriles \(1\) y \(2\)), en \(0.981\) s una nota corta y en \(1.450\) s una nota larga que dura \(0.75\) s.
  • Las notas están ordenadas por tiempo. Puede haber varias notas con el mismo tiempo (un acorde), pero no dos notas con el mismo tiempo y el mismo carril. Ninguna nota puede empezar en un carril mientras sigue sonando una nota larga de ese mismo carril (es decir, antes de tiempo + duración de esa nota larga).
  • No puede haber líneas vacías.

Al cargar un archivo de nivel, el juego debe verificar que respete todas estas reglas. Si no las respeta, debe lanzar una excepción descriptiva, que diga qué está mal y, cuando corresponda, en qué número de línea. Como mínimo, se deben detectar: líneas de nota sin \(2\) o \(3\) valores (por ejemplo, una línea vacía), valores que no son números, tiempos o duraciones negativos, carriles fuera de \(0\) a \(4\), notas desordenadas, notas repetidas, notas que empiezan durante una nota larga del mismo carril, encabezados sin : o después de las notas, encabezados obligatorios que faltan, beats inválidos y archivos sin notas. El juego debe atrapar esa excepción, mostrar el mensaje en pantalla y volver al menú, sin cerrarse. Gracias a este formato, los niveles también se pueden escribir o corregir a mano.

Archivos de nivel de ejemplo

charts_ejemplo.zip tiene los archivos de nivel, ya generados con la red, de tres canciones de licencia libre de Kevin MacLeod (licencia Creative Commons BY 4.0): “Hitman”, “Raving Energy” y “Severe Tire Damage”. Sirven para programar el juego antes de tener la red instalada. Para usarlos, hay que descargar las canciones, convertirlas a WAV con el mismo nombre que dice la clave cancion de cada archivo (por ejemplo, Raving Energy.wav), y poner los archivos de nivel en la carpeta charts/.

Dificultades#

Una dificultad nunca tiene más carriles ni más notas que la de arriba:

Dificultad Carriles Teclas
Fácil \(3\) (verde, rojo, amarillo) A, S, D
Medio \(4\) (verde a azul) A, S, D, F
Difícil \(5\) A, S, D, F, G
Experto \(5\) A, S, D, F, G

El nivel experto es el que generó la red. Los otros tres se arman con las reglas que usaban los diseñadores de Rock Band y Guitar Hero, que se explican en esta sección.

Cascada de dificultades
Ocho beats de "De música ligera" en las cuatro dificultades. Cada fila de puntos es un carril y las barras son notas largas

Resumen del método#

Las dificultades se arman en cascada, una a partir de la otra: el difícil a partir del experto, el medio a partir del difícil y el fácil a partir del medio. Para armar cada una se hacen dos cosas:

  1. Elegir qué golpes quedan (pasos 1 y 2, y en el difícil también el paso 3): se sacan golpes de la dificultad de arriba, empezando por los menos importantes, hasta que no quede ninguno demasiado cerca de otro.
  2. Calcular las notas de los golpes que quedaron (pasos 4, 5 y 6): en qué carriles va cada golpe, cuántas notas tiene si es un acorde y cuánto dura si es una nota larga.

Como cada dificultad sale de los golpes que quedaron en la de arriba, nunca puede tener un golpe que no esté en la de arriba.

Cómo se arman las dificultades
Cómo se arman las dificultades: los pasos 1, 2 y 3 (el 3, solo en el difícil) eligen golpes, y los pasos 4, 5 y 6 calculan las notas

Golpes#

Todos los pasos trabajan con golpes: todas las notas del experto que están en el mismo instante, juntas. Un golpe tiene tres datos:

  • su tiempo,
  • sus carriles: los carriles de sus notas en el experto,
  • su duración: la mayor de las duraciones de sus notas en el experto.

Por ejemplo, estas notas del experto:

[(1.450, 2, 0.75), (1.450, 4, 0), (1.919, 1, 0)]

forman dos golpes: uno en \(1.450\) s, con los carriles \(2\) y \(4\) y duración \(0.75\) s, y otro en \(1.919\) s, con el carril \(1\) y duración \(0\).

Duración de un beat#

Varias reglas hablan de distancias medidas “en beats”, por ejemplo “\(1/4\) de beat”. Para pasar eso a segundos hay que saber cuánto dura un beat. Pero los beats de una canción no están todos exactamente a la misma distancia entre ellos, así que para cada golpe se usa la duración del beat en el que está ese golpe. Se calcula así:

  1. El beat del golpe: se busca, en la lista de beats de la canción, el último beat que es menor o igual que el tiempo del golpe. Es el beat que “empezó” justo antes del golpe (o en el mismo instante).
  2. La duración del beat: es el tiempo del beat siguiente a ese menos el tiempo de ese beat. En las reglas, a este número se lo llama \(d\).

Hay dos casos especiales: si el golpe está antes del primer beat, se usa el primer beat; y si el golpe está en el último beat o después (y entonces no hay beat siguiente), se usa el anteúltimo beat.

Ejemplo

En “De música ligera”, los beats empiezan así: \(0.0\), \(0.28\), \(0.76\), \(1.243\), \(1.7226\), \(2.2022\ldots\) Para un golpe en \(1.6027\) s:

  1. Los beats menores o iguales que \(1.6027\) son \(0.0\), \(0.28\), \(0.76\) y \(1.243\). El último es \(1.243\): ese es el beat del golpe.
  2. El beat siguiente es \(1.7226\), así que la duración del beat es \(d = 1.7226 - 1.243 = 0.4796\) s.

Para ese golpe, “\(1/4\) de beat” son \(0.4796 / 4 = 0.1199\) s.

Importancia de un golpe#

No todos los golpes son igual de importantes: los que caen justo en un beat marcan el ritmo, y los que caen entre dos beats son adornos. Para saber dónde cae un golpe dentro de su beat se calcula su lugar:

lugar = (tiempo del golpe − tiempo de su beat) / duración del beat

El lugar es un número entre \(0\) y \(1\): vale \(0\) si el golpe cae justo en su beat, \(0.5\) si cae justo a la mitad entre su beat y el siguiente, y se acerca a \(1\) a medida que el golpe se acerca al beat siguiente. Según el lugar, cada golpe recibe un número de importancia (cuanto más chico, más importante). Para eso se compara el lugar con algunos números, con un margen de \(0.06\): el lugar “está a menos de \(0.06\)” de un número si la diferencia entre los dos (restando el mayor menos el menor) es menor que \(0.06\). Por ejemplo, \(0.46\) está a menos de \(0.06\) de \(0.5\), porque \(0.5 - 0.46 = 0.04\); en cambio, \(0.56\) no lo está, porque \(0.56 - 0.5 = 0.06\), que no es menor que \(0.06\).

Fila Si el lugar está a menos de \(0.06\) de… Es decir, si el lugar es… Importancia
1 \(0\) o \(1\) (en el beat) menor que \(0.06\), o mayor que \(0.94\) \(0\)
2 \(1/2\) \((0.5)\) mayor que \(0.44\) y menor que \(0.56\) \(1\)
3 \(1/3\) o \(2/3\) (\(0.333\ldots\) o \(0.666\ldots\)) mayor que \(0.2733\ldots\) y menor que \(0.3933\ldots\), o mayor que \(0.6066\ldots\) y menor que \(0.7266\ldots\) \(2\)
4 \(1/4\) o \(3/4\) (\(0.25\) o \(0.75\)) mayor que \(0.19\) y menor que \(0.31\), o mayor que \(0.69\) y menor que \(0.81\) \(3\)
5 ninguno de los anteriores cualquier otro valor \(4\)

Se revisan las filas en orden, de la 1 a la 5, y se usa la primera que se cumple. El orden importa porque las filas 3 y 4 se superponen: por ejemplo, un lugar de \(0.29\) cumple la fila 3 (está a \(0.0433\ldots\) de \(1/3\)) y también la fila 4 (está a \(0.04\) de \(0.25\)), pero como la fila 3 se revisa antes, su importancia es \(2\).

Si el golpe está antes del primer beat, o en el último beat o después, su importancia es \(4\) (no hace falta calcular el lugar).

Ejemplos

Con los beats de “De música ligera”:

Golpe en Su beat Beat siguiente Duración del beat Lugar Importancia
\(1.243\) s \(1.243\) \(1.7226\) \(0.4796\) \((1.243 - 1.243) / 0.4796 = 0\) \(0\) (fila 1)
\(1.6027\) s \(1.243\) \(1.7226\) \(0.4796\) \((1.6027 - 1.243) / 0.4796 = 0.75\) \(3\) (fila 4)
\(1.9624\) s \(1.7226\) \(2.2022\) \(0.4796\) \((1.9624 - 1.7226) / 0.4796 = 0.5\) \(1\) (fila 2)
Lugares dentro del beat
Los lugares dentro de un beat y su importancia

Los números de cada dificultad#

Los pasos usan tres números que cambian según la dificultad:

Dificultad Separación mínima \(S\) (pasos 1 y 2) Máximo de carriles por golpe (paso 5) Espacio después de una nota larga \(H\) (paso 6)
Difícil \(1/4\) de beat \(2\) \(1/4\) de beat
Medio \(1/2\) beat \(2\) \(1/2\) beat
Fácil \(1\) beat \(1\) \(1/4\) de beat

Paso 1: separación mínima#

Qué busca: que ningún golpe esté demasiado cerca de otro. Cuando dos golpes están demasiado cerca, queda el más importante.

Cómo se hace:

  1. Se toman los golpes de la dificultad de arriba y se calcula la importancia de cada uno.
  2. Se los ordena por importancia, de \(0\) a \(4\). Si dos golpes tienen la misma importancia, va primero el de menor tiempo.
  3. Se empieza con una lista vacía de golpes que quedan, y se recorren los golpes en el orden del punto 2. Para cada golpe:
    • se calcula la distancia mínima \(S \times d - 0.01\) segundos, donde \(d\) es la duración del beat de ese golpe (los \(0.01\) s son un margen, porque los tiempos están redondeados al milisegundo);
    • si el golpe está a esa distancia o más de todos los golpes que ya están en la lista de los que quedan, se lo agrega a la lista. Si no, se lo descarta.
  4. Al final, se ordenan los golpes que quedan por tiempo.

Ejemplo

En la dificultad media (\(S = 1/2\) beat), con un beat cada \(0.5\) s. Los golpes de la dificultad difícil están en \(0.0\), \(0.125\), \(0.25\) y \(0.5\) s, con importancias \(0\), \(3\), \(1\) y \(0\). La distancia mínima es \(1/2 \times 0.5 - 0.01 = 0.24\) s.

  • Orden por importancia: \(0.0\) (importancia \(0\)), \(0.5\) \((0)\), \(0.25\) \((1)\), \(0.125\) \((3)\).
  • \(0.0\): no hay ningún golpe en la lista, así que queda.
  • \(0.5\): está a \(0.5\) s de \(0.0\). Queda.
  • \(0.25\): está a \(0.25\) s de \(0.0\) y a \(0.25\) s de \(0.5\), las dos distancias son de \(0.24\) s o más. Queda.
  • \(0.125\): está a \(0.125\) s de \(0.0\), que es menos de \(0.24\) s. Se descarta.

Quedan \(0.0\), \(0.25\) y \(0.5\).

Paso 2: corridas, una sí y una no#

Qué busca: que no haya muchos golpes seguidos a la distancia mínima, que es lo más rápido que se permite. En esos casos queda un golpe sí y uno no. Así, el difícil queda mayormente con un golpe cada \(1/2\) beat, el medio con uno por beat y el fácil con uno cada dos beats.

Cómo se hace:

  1. Se recorren los golpes que quedaron del paso 1, en orden de tiempo, y se los separa en corridas: un golpe sigue la corrida del anterior si la distancia entre los dos es \(S \times d\) segundos, con un margen de \(0.01\) s para arriba o para abajo (\(d\) es la duración del beat del golpe anterior). Si no, empieza una corrida nueva.
  2. Las corridas de \(1\) o \(2\) golpes quedan como están.
  3. En cada corrida de \(3\) o más golpes, hay dos opciones: quedarse con los golpes 1.º, 3.º, 5.º… de la corrida (opción A) o con los golpes 2.º, 4.º, 6.º… (opción B). Se elige:
    1. la que tenga el menor promedio de importancia de sus golpes;
    2. si empatan, la que tenga la mayor proporción de golpes que son acordes (más de un carril) o notas largas (duración mayor que \(0\));
    3. si siguen empatadas, la opción A.

Ejemplo

Sigue el ejemplo del paso 1: los golpes \(0.0\), \(0.25\) y \(0.5\) están cada \(0.25\) s, que es \(S \times d = 1/2 \times 0.5\). Forman una corrida de \(3\) golpes. La opción A es {\(0.0\), \(0.5\)}, con importancias \(0\) y \(0\) (promedio \(0\)). La opción B es {\(0.25\)}, con importancia \(1\) (promedio \(1\)). Gana la A: quedan \(0.0\) y \(0.5\).

Paso 3: un respiro en difícil#

Qué busca: que en el difícil no haya demasiados golpes seguidos a \(1/2\) beat. Este paso solo se hace en el difícil.

Cómo se hace: se separan los golpes en corridas como en el punto 1 del paso 2, pero con \(1/2\) beat en lugar de \(S\). En cada corrida se sacan el 8.º, el 16.º, el 24.º… golpe. Es decir, nunca quedan más de \(7\) golpes seguidos a \(1/2\) beat.

Ejemplo

Si hay \(9\) golpes seguidos a \(1/2\) beat, se saca el 8.º y quedan el 1.º al 7.º y el 9.º.

Paso 4: carriles#

Qué busca: pasar los \(5\) carriles del experto a los \(4\) del medio o los \(3\) del fácil, sin que cambie la “forma” de la canción: si en el experto la mano se mueve hacia la derecha, en las otras dificultades nunca se mueve hacia la izquierda.

Para eso se usa una traducción: una tupla de \(5\) números que dice a qué carril va cada carril del experto. El primer número dice a qué carril va el verde \((0)\) del experto, el segundo a cuál va el rojo \((1)\), el tercero a cuál va el amarillo \((2)\), y así. Por ejemplo, la traducción (0, 1, 1, 2, 2) para el fácil manda el verde \((0)\) al verde, el rojo \((1)\) y el amarillo \((2)\) al rojo, y el azul \((3)\) y el naranja \((4)\) al amarillo.

Traducción de carriles
Dos traducciones posibles, una para el medio y una para el fácil

Estas son todas las traducciones posibles (las únicas que respetan el orden de izquierda a derecha):

Medio (\(4\) carriles) Fácil (\(3\) carriles)
(0, 1, 2, 3, 3) (0, 1, 2, 2, 2)
(0, 1, 2, 2, 3) (0, 1, 1, 2, 2)
(0, 1, 1, 2, 3) (0, 1, 1, 1, 2)
(0, 0, 1, 2, 3) (0, 0, 1, 2, 2)
(0, 0, 1, 1, 2)
(0, 0, 0, 1, 2)

El difícil tiene los mismos \(5\) carriles que el experto, así que su traducción es siempre (0, 1, 2, 3, 4).

Qué traducción usar. El medio y el fácil usan, cada uno, una sola traducción para toda la canción: la que pierde menos cambios de carril del experto.

  • Un cambio de carril del experto es un par de golpes consecutivos del experto (de todos sus golpes, no solo de los que quedaron) cuyos carriles más bajos son distintos.
  • Una traducción pierde ese cambio si manda esos dos carriles al mismo carril.
  • Si varias traducciones pierden la misma cantidad de cambios, se usa la más grande de ellas, comparándolas número por número de izquierda a derecha: la primera posición en la que difieren decide. Por ejemplo, (0, 1, 2, 2, 3) es más grande que (0, 1, 1, 2, 3), porque son iguales hasta el segundo número y en el tercero \(2\) es mayor que \(1\).

Ejemplo

En la dificultad media, si los carriles más bajos de los golpes del experto son, en orden, \(0\), \(1\), \(2\), \(3\), \(4\), \(3\):

Traducción Secuencia traducida Cambios perdidos
(0, 1, 2, 3, 3) \(0, 1, 2, 3, 3, 3\) \(2\) (de \(3\) a \(4\) y de \(4\) a \(3\): los dos van al carril \(3\))
(0, 1, 2, 2, 3) \(0, 1, 2, 2, 3, 2\) \(1\) (de \(2\) a \(3\): los dos van al carril \(2\))
(0, 1, 1, 2, 3) \(0, 1, 1, 2, 3, 2\) \(1\) (de \(1\) a \(2\): los dos van al carril \(1\))
(0, 0, 1, 2, 3) \(0, 0, 1, 2, 3, 2\) \(1\) (de \(0\) a \(1\): los dos van al carril \(0\))

Hay un empate en \(1\) entre las tres últimas, y la más grande es (0, 1, 2, 2, 3). Entonces esa es la traducción que se usa para toda la canción.

Paso 5: acordes#

Recordatorio: un acorde son dos o más notas en el mismo instante, en carriles distintos; es decir, un golpe con más de un carril.

Qué busca: que los acordes no sean demasiado difíciles: como mucho \(2\) carriles, y ningún acorde en el fácil.

Cómo se hace, para cada golpe que tiene más de un carril:

  • en el fácil (máximo \(1\) carril), queda solo el carril más bajo;
  • en el difícil y el medio (máximo \(2\) carriles), quedan el más bajo y el más alto.

Ejemplos

  • Medio con la traducción (0, 1, 2, 3, 3), golpe con los carriles \(0\), \(2\) y \(4\): van a \(0\), \(2\) y \(3\). Quedan el más bajo y el más alto: un acorde en los carriles \(0\) y \(3\).
  • Fácil con la traducción (0, 1, 1, 2, 2), golpe con los carriles \(1\) y \(3\): van a \(1\) y \(2\). En el fácil queda solo el más bajo: una nota en el carril \(1\).

Paso 6: notas largas#

Qué busca: que después de una nota larga quede tiempo para soltar la tecla antes del golpe siguiente.

Cómo se hace, para cada golpe que quedó, partiendo de su duración del experto (y con \(d\) la duración de su beat):

  1. Si hay un golpe siguiente (en esta dificultad), la duración no puede ser mayor que (tiempo del golpe siguiente − su tiempo − \(H \times d\)). Si es mayor, se baja su duración a ese valor.
  2. Si la duración, recortada o no, es menor que \(d / 4\) (\(1/4\) de beat), pasa a ser 0: es una nota corta.
  3. La duración se redondea a \(3\) decimales.

Todas las notas del golpe tienen esa duración.

Ejemplos

En el medio (\(H = 1/2\) beat), con un beat cada \(0.5\) s: \(H \times d = 0.25\) s y \(d / 4 = 0.125\) s.

  • Golpe en \(1.0\) s con duración \(0.9\) s, y el siguiente golpe en \(1.5\) s: el máximo es \(1.5 - 1.0 - 0.25 = 0.25\) s, así que la duración baja a \(0.25\). Como no es menor que \(0.125\), queda como nota larga de \(0.25\) s.
  • Golpe en \(1.0\) s con duración \(0.9\) s, y el siguiente golpe en \(1.25\) s: el máximo es \(1.25 - 1.0 - 0.25 = 0\), así que la duración baja a \(0\). Es una nota corta.

Ejemplo completo#

Una canción a \(120\) bpm tiene un beat cada \(0.5\) s: los beats están en \(0.0\), \(0.5\), \(1.0\), …, \(4.0\) s. Este es su nivel experto, ya agrupado en golpes:

Tiempo (s) Carriles Duración Importancia
\(0.000\) \(0\) \(0\) \(0\)
\(0.125\) \(1\) \(0\) \(3\)
\(0.250\) \(2\) \(0\) \(1\)
\(0.375\) \(1\) \(0\) \(3\)
\(0.500\) \(0\), \(2\), \(4\) \(0\) \(0\)
\(1.000\) \(3\) \(0.9\) \(0\)
\(2.000\) \(1\) \(0\) \(0\)
\(2.250\) \(2\) \(0\) \(1\)
\(2.500\) \(1\) \(0\) \(0\)
\(2.750\) \(2\) \(0\) \(1\)
\(3.000\) \(3\) \(0\) \(0\)
\(3.250\) \(4\) \(0\) \(1\)
\(3.500\) \(3\) \(0\) \(0\)

Difícil (\(S = 1/4\) de beat = \(0.125\) s, a partir del experto):

  • Paso 1: todos los golpes están a \(0.125\) s o más de los demás, así que quedan todos.
  • Paso 2: \(0.000\), \(0.125\), \(0.250\), \(0.375\) y \(0.500\) forman una corrida de \(5\) golpes separados por \(0.125\) s. Las dos opciones son {\(0.000\), \(0.250\), \(0.500\)}, con importancias \(0\), \(1\) y \(0\) (promedio \(0.33\)), y {\(0.125\), \(0.375\)}, con importancias \(3\) y \(3\) (promedio \(3\)). Queda la primera. No hay otras corridas a \(0.125\) s.
  • Paso 3: los golpes de \(2.000\) a \(3.500\) están cada \(0.25\) s (\(1/2\) beat), pero son \(7\): no se saca ninguno.
  • Pasos 4 y 5: la traducción es (0, 1, 2, 3, 4). El acorde de \(0.500\) tiene los carriles \(0\), \(2\) y \(4\): quedan el más bajo y el más alto, \(0\) y \(4\).
  • Paso 6: la nota larga de \(1.000\) dura \(0.9\) s, pero el golpe siguiente está en \(2.000\) y \(H \times d = 0.125\) s. Se la baja a \(2.000 - 1.000 - 0.125 = 0.875\) s.

Resultado: \(11\) golpes, en \(0.000\), \(0.250\), \(0.500\), \(1.000\), \(2.000\), \(2.250\), \(2.500\), \(2.750\), \(3.000\), \(3.250\) y \(3.500\).

Medio (\(S = 1/2\) beat = \(0.25\) s, a partir del difícil):

  • Paso 1: los golpes del difícil ya están a \(0.25\) s o más entre sí: quedan todos.
  • Paso 2: hay dos corridas a \(0.25\) s. En {\(0.000\), \(0.250\), \(0.500\)}, las opciones son {\(0.000\), \(0.500\)} (promedio \(0\)) y {\(0.250\)} (promedio \(1\)): queda {\(0.000\), \(0.500\)}. En los \(7\) golpes de \(2.000\) a \(3.500\), las opciones son {\(2.000\), \(2.500\), \(3.000\), \(3.500\)} (promedio \(0\)) y {\(2.250\), \(2.750\), \(3.250\)} (promedio \(1\)): queda la primera.
  • Paso 4: el carril más bajo de cada golpe del experto es, en orden: \(0\), \(1\), \(2\), \(1\), \(0\), \(3\), \(1\), \(2\), \(1\), \(2\), \(3\), \(4\), \(3\). La traducción (0, 1, 2, 3, 3) pierde \(2\) cambios (de \(3\) a \(4\) y de \(4\) a \(3\)), (0, 1, 2, 2, 3) pierde \(1\) (de \(2\) a \(3\)), (0, 1, 1, 2, 3) pierde \(5\) y (0, 0, 1, 2, 3) pierde \(2\). Se elige (0, 1, 2, 2, 3).
  • Paso 5: el acorde de \(0.500\) (carriles \(0\), \(2\) y \(4\) del experto) se traduce a \(0\), \(2\) y \(3\), y quedan el más bajo y el más alto: \(0\) y \(3\). El golpe de \(1.000\) (carril \(3\)) va al carril \(2\), y los de \(3.000\) y \(3.500\) también.
  • Paso 6: la nota larga de \(1.000\) se baja a \(2.000 - 1.000 - 0.25 = 0.75\) s.

Resultado: \(7\) golpes, en \(0.000\), \(0.500\), \(1.000\), \(2.000\), \(2.500\), \(3.000\) y \(3.500\).

Fácil (\(S = 1\) beat = \(0.5\) s, a partir del medio):

  • Paso 1: los golpes del medio están a \(0.5\) s o más entre sí: quedan todos.
  • Paso 2: hay dos corridas a \(0.5\) s. En {\(0.000\), \(0.500\), \(1.000\)}, las opciones son {\(0.000\), \(1.000\)} y {\(0.500\)}: las dos tienen promedio de importancia \(0\). En la primera, la mitad de los golpes es acorde o nota larga (el de \(1.000\)); en la segunda, todos (el de \(0.500\) es un acorde): queda {\(0.500\)}. En {\(2.000\), \(2.500\), \(3.000\), \(3.500\)}, las dos opciones empatan en todo, así que queda la primera: {\(2.000\), \(3.000\)}.
  • Paso 4: (0, 1, 2, 2, 2) y (0, 0, 1, 1, 2) pierden \(3\) cambios cada una, y las otras más. De esas dos, la más grande es (0, 1, 2, 2, 2).
  • Paso 5: el golpe de \(0.500\) queda en su carril más bajo, el \(0\). El de \(2.000\) (carril \(1\)) va al \(1\) y el de \(3.000\) (carril \(3\)) va al \(2\).

Resultado: \(3\) golpes, en \(0.500\) (carril \(0\)), \(2.000\) (carril \(1\)) y \(3.000\) (carril \(2\)).

Ejemplo completo
El ejemplo completo en las cuatro dificultades

Jugabilidad#

Las notas aparecen arriba de la pantalla y bajan por su carril hasta una línea de golpe, a la que llegan justo en su tiempo. Cada nota tarda \(1.5\) s en bajar desde que aparece hasta la línea de golpe, en todas las dificultades.

Sincronización con la música

La posición de cada nota en la pantalla debe calcularse a partir del tiempo transcurrido de la canción (por ejemplo, con pygame.time.get_ticks(), descontando el tiempo que estuvo en pausa), y no sumando un desplazamiento fijo en cada frame. Si se suma un desplazamiento por frame, cualquier frame que tarde más de lo esperado hace que las notas se atrasen respecto de la música.

Cada vez que el jugador aprieta la tecla de un carril, se busca, entre las notas de ese carril que todavía no se juzgaron, la de tiempo más cercano al instante en que se apretó la tecla:

Diferencia entre la tecla y la nota más cercana Resultado
hasta \(50\) ms Perfecto
más de \(50\) ms y hasta \(100\) ms Bien
no hay ninguna nota a \(100\) ms o menos Nota falsa

Si pasan más de \(100\) ms desde el tiempo de una nota y no se la tocó, la nota es una fallada.

En una nota larga, el comienzo se juzga igual que en una nota corta. Si el comienzo fue perfecto o bien, el jugador tiene que mantener la tecla apretada hasta el final de la nota (su tiempo más su duración):

  • Mientras la mantiene, suma puntos (ver Puntuación).
  • Si la suelta cuando faltan \(100\) ms o menos para el final, o si llega al final sin soltarla, la nota larga está completada.
  • Si la suelta antes, la nota larga queda cortada: deja de sumar puntos, pero no se pierde el combo.
  • Si el comienzo fue una fallada, toda la nota larga es una fallada.

La partida empieza con una cuenta regresiva de \(3\) segundos antes de que empiece la música. Termina cuando todas las notas ya se juzgaron (y no se está manteniendo ninguna nota larga) y, además, terminó la canción o pasaron \(1.5\) segundos desde la última nota. También termina, perdida, si la energía llega a \(0\) (ver Puntuación).

Puntuación#

Resultado Puntos Energía
Perfecto \(100\) \(+2\)
Bien \(50\) \(+1\)
Fallada \(0\) \(-6\)
Nota falsa \(0\) \(-3\)
  • Combo: cantidad de perfectos y bien seguidos. Una fallada o una nota falsa lo vuelven a \(0\).
  • Multiplicador: los puntos de cada perfecto o bien se multiplican por min(4, 1 + combo // 10), usando el combo de antes de esa nota. Es decir: \(\times 1\) en las primeras \(10\) notas seguidas, \(\times 2\) en las \(10\) siguientes, \(\times 3\) en las \(10\) siguientes y \(\times 4\) desde la nota \(31\).
  • Notas largas: mientras se mantiene una nota larga, se suman \(100\) puntos por segundo, multiplicados por el multiplicador actual. Completarla suma \(+2\) de energía.
  • Energía: empieza en \(50\), nunca baja de \(0\) ni sube de \(100\). Si llega a \(0\), el público abuchea al jugador: la música se corta y la partida se pierde.
  • Precisión: (perfectos + \(0.5\) × bien) / cantidad de notas. Por ejemplo, con \(80\) perfectos y \(10\) bien en una canción de \(100\) notas, la precisión es \((80 + 0.5 \times 10) / 100 = 0.85\). Cada nota de un acorde cuenta como una nota.
  • Estrellas: al terminar una canción sin perder, se otorgan estrellas según la precisión:
Precisión Estrellas
\(95\) % o más ★★★★★
\(85\) % o más ★★★★☆
\(70\) % o más ★★★☆☆
\(50\) % o más ★★☆☆☆
menos de \(50\) % ★☆☆☆☆

Cada partida terminada sin perder se agrega como una línea al archivo puntajes.csv, que tiene esta primera línea y una línea por partida:

cancion,dificultad,puntaje,precision,fecha
believer.wav,medio,48250,0.912,2026-11-20 18:35

La dificultad se escribe como facil, medio, dificil o experto; la precisión, como un número entre \(0\) y \(1\) con \(3\) decimales; y la fecha, con el formato año-mes-día hora:minutos. El récord de una canción en una dificultad es el mayor puntaje de las líneas de esa canción y esa dificultad (\(0\) si no hay ninguna).

Pantallas#

El juego debe tener, como mínimo, las siguientes pantallas:

  • Menú principal: muestra las canciones (archivos .wav) que hay en la carpeta canciones/, agrupadas por género. Cada subcarpeta de canciones/ es un género (por ejemplo, canciones/Metal/chop_suey.wav es del género “Metal”), y los .wav que estén sueltos en canciones/ forman el grupo “Sin género”. Se debe poder pasar de una canción a la anterior o a la siguiente, saltar directamente al género anterior o al siguiente, y elegir una canción.
  • Selección de dificultad: fácil, medio, difícil o experto, mostrando el récord de la canción en cada una.
  • Generación: si la canción no tiene archivo de nivel, se lo genera con la red. Mientras tanto, la pantalla debe avisar que se está generando el nivel (por ejemplo, con el texto “Generando el nivel…”). Es opcional mostrar además una barra de progreso que se llene según la fracción completada, el texto que dice qué está haciendo la red, y permitir cancelar la generación con ESC para volver a la selección de dificultad.
  • Partida, con la cuenta regresiva.
  • Pausa: con ESC se pausa la partida (y la música), y se puede continuar o volver al menú.
  • Resultados: puntaje, precisión, estrellas, cantidad de perfectos, bien, falladas y notas falsas, notas largas completadas (de cuántas había), combo máximo y si se batió el récord. Si el jugador perdió, debe quedar claro.
La pantalla de generación

Mientras generar trabaja, el resto del programa queda esperando: el loop principal del juego no corre. Por eso, el aviso de que se está generando el nivel hay que dibujarlo (y llamar a pygame.display.flip()) antes de llamar a generar. Mientras la red trabaja, la ventana no responde, y el sistema operativo puede mostrar un aviso de que el programa no responde: es normal.

Para la parte opcional, se puede dibujar la pantalla desde la función de progreso: cada vez que la red la llama, dibujar la barra, llamar a pygame.display.flip() y leer los eventos con pygame.event.get(). Leer los eventos hace que la ventana siga respondiendo. Ahí mismo se puede ver si se apretó ESC y, en ese caso, lanzar una excepción para cancelar.

Sonido#

Durante la partida debe sonar la canción, que se reproduce con pygame.mixer.music. Es opcional agregar efectos de sonido (por ejemplo, para las notas falladas, las notas falsas o para moverse por los menús).

Canciones#

Las canciones tienen que estar en formato WAV.

Interfaz#

La interfaz debe estar implementada con PyGame y mostrar claramente:

  • Los carriles de la dificultad con sus colores, la línea de golpe y qué teclas están apretadas.
  • Las notas bajando. Las notas largas deben mostrar su duración (por ejemplo, con una “cola” detrás de la nota) y dejar claro cuándo se están manteniendo y cuándo se cortaron.
  • El resultado de cada nota (perfecto, bien, fallada, nota falsa, nota larga cortada).
  • El puntaje, el récord, el combo, el multiplicador y la energía.
  • Cuánto falta para que termine la canción.
Juego de referencia
Ejemplo simple de cómo podría verse el juego codeado por la cátedra

Personalización

Son libres de personalizar la estética del juego: colores, forma de las notas, temática, efectos visuales, etc. Lo importante es que los niveles, la jugabilidad y la puntuación sean fieles a esta consigna y cumplan con los requisitos mínimos.

Recursos útiles#

Entrega#

El trabajo se realizará en grupos de no más de cuatro estudiantes. El desarrollo del trabajo se realizará en un repositorio privado de Github que debe ser compartido a los docentes de la materia (pueden encontrar los usuarios de los docentes aquí).

Además, cada grupo debe realizar un video de no más de 5 minutos mostrando el funcionamiento del programa. En el video deben mostrarse todas las funciones del programa y cómo se juega una partida completa, incluyendo la generación del nivel de una canción nueva y cómo quedan sus cuatro dificultades. Debe quedar claro qué se está mostrando; de ser necesario, pueden agregar narración en off. El video debe ser subido a YouTube como “no listado”. La calidad del video tendrá un impacto leve en la nota final.

La entrega del trabajo se realizará subiendo el link del repositorio y del video de YouTube a la tarea correspondiente en el campus del curso. La fecha de entrega se encuentra en la misma tarea y en el calendario del curso.

Se recuerda a los estudiantes que las entregas deben ser un producto original de cada estudiante, por lo que se les pide revisar la sección 6 del programa de la materia y el Código de Honor y Ética.

Evaluación#

A diferencia de los trabajos prácticos anteriores, este trabajo práctico tiene una nota numérica individual asociada. La misma será definida por los docentes de la materia y se basará en la calidad del trabajo entregado y en la defensa oral del mismo.

Para aprobar, es fundamental que el código se ejecute correctamente sin lanzar excepciones; cumpliendo con los requerimientos de la consigna. Además, se evaluará la calidad del código y la calidad de los comentarios y documentación. Cualquier detalle adicional que agreguen será tenido en cuenta para la nota final.

La defensa oral se realizará en la semana de finales de la materia. En la misma, se les pedirá a los estudiantes que presenten su trabajo y respondan preguntas sobre el mismo. No es necesario que se arme una presentación, pero es recomendable que traigan alguna computadora con el código para poder verlo junto al docente en el momento. La defensa oral es obligatoria y es un requisito para aprobar la materia.