TP1: Sixteen

Para el primer trabajo práctico se solicita desarrollar una versión del juego Sixteen de la colección de Simon Tatham realizado en Python y que se pueda jugar desde la terminal.

Explicación del juego

Es un sencillo juego donde el objetivo es ordenar todos los números de izquierda a derecha y de arriba a abajo dado un tablero de dimensiones N x M. Para ello el jugador puede mover las fichas con operaciones de rotaciones de filas y columnas.

Las rotaciones se pueden hacer en cualquier dirección, y hará que todos los elementos de la fila o columna se muevan hacia dicha dirección, con el elemento más cercano del borde terminando del otro lado de la fila o columna. Por ejemplo, así sería el antes y el después de una rotación a la izquierda de la fila seleccionada para el siguiente tablero:

Ejemplo

El contenido de la fila era originalmente 4, 11, 5, 6 y se cambió a 11, 5, 6, 4 con la rotación a la izquierda. Como el 4 estaba contra el borde izquierdo, se re-posicionó al final de la fila.

El juego se considera completado si todos los elementos están ordenados de izquierda a derecha.

Objetivo del trabajo práctico

Se busca realizar una implementación del Sixteen que se pueda jugar de inicio a fin, desde la terminal, permitiendo tableros de cualquier dimensión NxM con N y M mayores o iguales a dos y menores a diez.

El trabajo práctico se divide en dos partes:

  • Parte 1: se implementa la lógica del juego mediante funciones de un módulo sixteen.py. No incluye entrada, salida, ni interacción con el usuario (sin print, input, etc).
  • Parte 2: se implementa el programa dentro de un archivo main.py. Utiliza las funciones elaboradas del módulo sixteen.py para que, al ejecutarse el main.py, el usuario pueda interactuar con el juego de inicio a fin.

Material

Primera parte: Lógica del juego

La primera parte del trabajo práctico consiste en implementar la lógica del juego. Para ello se deberán implementar las funciones que están definidas dentro del archivo sixteen.py de modo de cumplir con el contrato que especifica su documentación.

Las diferentes funciones nos permitirán llevar las responsabilidades del juego hacia código. Cada una responde a una acción o pregunta:

  • Acción - empezar un nuevo juego: crear_tablero
  • Acción - realizar una rotación hacia la izquierda: rotar_izquierda
  • Acción - realizar una rotación hacia la derecha: rotar_derecha
  • Acción - realizar una rotación hacia arriba: rotar_arriba
  • Acción - realizar una rotación hacia abajo: rotar_abajo
  • Acción - desordenar el tablero: mezclar_tablero
  • Pregunta - ¿está ordenado el tablero?: esta_ordenado

Las funciones definidas en el sixteen.py serán útiles para:

  • Manejar el programa de la segunda parte del trabajo práctico
  • Reutilizarlas y facilitar la lógica de otras funciones dentro del mismo archivo sixteen.py
  • Verificar el funcionamiento del módulo en su totalidad, mediante el archivo sixteen.py

Se admite crear otras funciones auxiliares en el archivo sixteen.py si se considera necesario, pero no se permite eliminar las funciones definidas en el archivo base, ni tampoco cambiar sus parámetros o tipo de retorno.

Adicionalmente, tampoco se permite el uso de variables globales (pero sí el de constantes).

El detalle específico sobre qué debe hacer cada función está indicado en su respectiva documentación. Dentro del archivo existen varias definiciones como la siguiente:

def crear_tablero(n_filas: int, n_columnas: int) -> list[list[int]]:
    """
    Crea un tablero ordenado, con dimensiones `n_filas` por `n_columnas`.

    El tablero estará representado como una lista de listas de enteros. El
    primer número en la posición `[0][0]` será un 1, el de la izquierda será un
    2, y así sucesivamente hasta completar todos los casilleros, sin repetir
    los números, hasta llegar al número `n_filas * n_columnas`.

    PRECONDICIONES:
        - `n_filas` y `n_columnas` son enteros positivos mayor a uno y menores
          a diez.

    POSTCONDICIONES:
        - la función devuelve un nuevo tablero ordenado de enteros que se puede
        utilizar para llamar al resto de las funciones del módulo.

    EJEMPLO:
        >>> crear_tablero(4, 5)
        [
            [1, 2, 3, 4, 5],
            [6, 7, 8, 9, 10],
            [11, 12, 13, 14, 15],
            [16, 17, 18, 19, 20],
        ]
    """

La estructura del tablero

La primera función a implementar será crear_tablero, y habrás notado que se espera un valor del tipo de dato ya definido: una lista de listas de enteros. No se puede cambiar el tipo de dato del tablero, debe ser explícitamente una lista de listas de enteros.

Dicha estructura nos servirá para llevar adelante toda la lógica del juego. El resto de las funciones del módulo reciben un tablero que esperan esta estructura.

Generación de tableros y sobre la función de mezclar_tablero

Al iniciar un nuevo juego, tenemos la particularidad de que no todo tablero NxM con posiciones aleatorias se puede resolver. Tenemos que tener cuidado a la hora de posicionar los elementos del tablero porque buscamos garantizar que todo tablero pueda solucionarse. Para tener esta garantía, podemos aplicar el proceso "inverso" a resolver un tablero: mezclarlo!

El proceso para generar un nuevo juego desordenado sería el siguiente:

  1. crear_tablero: generar un juego que ya se encuentre resuelto por tener los números ordenados
  2. mezclar_tablero: Realizar Z cantidad de rotaciones aleatorias al mismo juego de forma secuencial, siendo Z una cantidad de movimientos elevada para asegurar que nuestro tablero esté "alejado" de la solución.

De esta forma, el juego siempre será resolvible ya que realizar los mismos movimientos pero en orden inverso nos daría un juego resuelto.

Para la implementación de mezclar_tablero, se permite utilizar el módulo random de la biblioteca estándar de Python. Se recomienda investigar, por ejemplo, las funciones random.random(), random.choice(), random.randint().

Pruebas

Para saber si su implementación está funcionando correctamente deberán ejecutar el archivo sixteen.py, que correrá un conjunto de pruebas automáticas y les indicará si su implementación está funcionando correctamente.

Para utilizarlo, copiá el archivo sixteen_test.py en la misma carpeta que sixteen.py, y ejecutá:

python3 sixteen_test.py

Nota: Las pruebas automáticas están para simplificar el desarrollo y ofrecerles un feedback rápido sobre si su código está funcionando o no. Es necesario para la entrega que las pruebas pasen, pero esto no implica que el trabajo esté aprobado. Las pruebas no contemplan todos los casos posibles.

Cada prueba es una función dentro del archivo sixteen.py. Si alguna prueba falla, te recomendamos que leas el código de la prueba para ver cuál es el caso que falla y cómo solucionar el problema.

Segunda parte: Interfaz de usuario

Finalizada la lógica de juego, deberán implementar una interfaz de usuario que permita jugar al sixteen en un nuevo archivo main.py. El programa debe pedirle al usuario las dimensiones N y M para hacer el tamaño del tablero NxM (con N y M entre 2 y 9) para luego comenzar un nuevo juego generado. El programa finaliza cuando el tablero se encuentre ordenado.

Los requerimientos para la interfaz son:

  • Debe mostrarse el tablero en un formato agradable para el usuario.
    • Con "un formato agradable" sería evitando el uso de un simple print(tablero) o algún uso de print que resulte en la visualización de corchetes o comas resultantes por imprimir una lista de Python.
    • [Opcional] puede incluirse una ayuda visual para identificar las coordenadas de la celda, a los bordes del tablero.
  • Una vez creado el tablero, se le debe solicitar al usuario una rotación nueva.
    • El formato de la entrada solicitada al usuario queda a criterio propio con tal de que sea posible hacer cualquier tipo de rotación sobre todas las filas/columnas.
    • Si la operación no es exitosa, se deberá indicar al usuario con un mensaje.
  • En cualquier momento a lo largo de la ejecución el programa no debe explotar. El código también debe realizar todas las validaciones correspondientes para que las entradas del usuario no finalicen el programa con un error.
  • Debe incluirse una tecla para finalizar el juego actual.

A continuación les dejamos un ejemplo de una interfaz de usuario para que tengan como referencia. No es necesario que sea idéntica, tómenla como una guía.

Demo

Recomendaciones

  • Investigar el uso de las funciones str.ljust y str.rjust para mostrar un tablero alineado. No es necesario su uso, pero tenerlas en cuenta.
  • El Sixteen como juego es bastante complicado de resolver. Para el desarrollo del programa, se recomienda probar tableros de tamaño menor, o ajustar la cantidad de movimientos Z de la función mezclar_tablero para que el tablero generado esté "casi" resuelto (recordar re-ajustar el Z antes de entregar!).
  • Recordar que sixteen es un módulo y debe importarse/usarse en main.py como tal:
import sixteen

def main():
    tablero = sixteen.crear_tablero(...)
    sixteen.mezclar_tablero(tablero)
    ...

main()

Restricciones y condiciones de entrega

  • La entrega debe pasar todas las pruebas para ser considerada como tal.
  • El código debe estar correctamente modularizado y seguir las convenciones y buenas prácticas establecidas por el lenguaje y el curso.
  • El uso de módulos de la biblioteca estándar de Python deberá ser previamente autorizado por un docente.
    • Se puede utilizar el módulo random sin autorización previa.
  • No se permite el uso de ninguna biblioteca externa.
  • No se permite el uso del operador de argumentos * de la forma funcion(*argumentos).
  • No se permite el uso de variables globales (sí el de constantes!).
  • No se permite el uso de eval.

Entrega

La corrección de los trabajos a partir del TP1 serán por la plataforma GitHub. Para esto será necesario crearse una cuenta de GitHub e indicarnos el usuario antes de realizar la entrega al sistema.

En total, la secuencia completa de entrega y corrección tiene los siguientes pasos:

  1. Llená el siguiente formulario para indicarnos la cuenta de GitHub. Si no tenés cuenta, te podés crear una en https://github.com/ usando el mail que quieras.
  2. Subí los archivos sixteen.py y main.py en nuestra página de entregas.
  3. Una vez realizada la entrega te debería llegar un mail de una invitación a un repositorio privado de GitHub con tu entrega, generado por nuestro sistema. Será necesario aceptar la invitación pues la misma vence a los 7 días una vez realizada.
  4. Tu ayudante hará la corrección en este repositorio de GitHub y te lo indicará al estar disponible.

Nota: Los trabajos prácticos NO deben subirse a un repositorio público de GitHub. Una entrega de trabajo práctico accesible al público correría el riesgo de ser copiada, y bajo el régimen de cursada implicaría la desaprobación de ambos trabajos prácticos (el original y la copia).