Programación 1 - Trabajo Práctico 1
Este es el primer trabajo práctico de la asignatura. Recordá que, tal como se aclaró en clase, una condición para regularizar la materia es tener aprobado los trabajos prácticos.
Forma de entrega: A través de la plataforma.
Fecha límite: 30/05. No se considerará aprobado un trabajo que haya sido entregado luego de esta fecha.
El trabajo puede hacerse de forma individual o en grupos de 2 personas. Desde la cátedra recomendamos esta última opción.
Por cualquier duda te recomendamos acercarte a consulta. Desde la cátedra podemos ayudarlos a resolver cualquiera de las consignas, e incluso a organizar la solución. También podés hacer preguntas a través de la plataforma, pero mediante mensajes privados a los docentes. No uses el foro para consultas sobre este práctico, pues podrías no cumplir con la siguiente condición.
No está permitido compartir código con otro grupo. Esto incluye signaturas, declaraciones de propósito, casos de test y, obviamente, definiciones de funciones. Desde la cátedra se revisará (ayudados por un software desarrollado a tal fin) que todos los grupos se hayan ajustado a esta condición. No cumplir con esta restricción tiene como consecuencia inmediata la no aprobación del trabajo.
Los ejercicios opcionales no se tienen en cuenta para la nota final. Es decir, un grupo puede obtener un 10 como calificación sin necesidad de haber resuelto ninguno de los ítems opcionales.
Quizás no puedas resolver todos los ítems. Sin embargo, si seguís la receta de diseño, podés obtener una solución parcial que sólo incluye parte de la funcionalidad requerida. A modo de ejemplo, puede suceder que no puedas resolver la parte que controla el tiempo restante. No tendrás un sobresaliente, pero si el resto de las cosas funciona, seguramente aprobarás el trabajo. Recordá que siempre es mejor esta situación a entregarlo fuera del plazo establecido.
Lo repetimos porque es muy importante. Vení a consulta ante la primer dificultad que encuentres.
1 Introducción
A lo largo de este trabajo práctico implementaremos un juego simple, en el cual un personaje recorre un laberinto buscando alcanzar un objetivo. Iniciaremos con las funcionalidades básicas, y luego iremos incorporando diferentes extensiones.
En el siguiente video se puede ver la versión final: (muestra.mp4)
A lo largo del diseño del programa, deberá seguir fielmente la "receta de diseño", acompañando al código con la adecuada documentación. A la hora de la evaluación, no solo se analizarán los requerimientos funcionales, sino también la correcta organización del código, los criterios a la hora de las definiciones, etc.
A modo de guía en la organización del trabajo, se presenta una plantilla ("template.rkt") con la estructura principal del programa y sus principales funciones. Claramente se puede modificar y agregar definiciones de constantes y funciones auxiliares en caso de necesitarlas, con un criterio adecuado.
Para implementar el juego, contaremos con la función interseca?, importada del módulo "extras.rkt". Como nos aclara su definición de propósito, esta función determina si dos imágenes se superponen en la escena:
; interseca? : Posn Image Posn Image -> Boolean ; (interseca? p1 img1 p2 img2) Determina si img1 en la posición p1 se interseca ; con img2 en la posición p2. Al decir "interseca" nos referimos a si cambia el ; resultado de acuerdo al orden en que son dibujadas las imágenes.
Esta función será útil en varias partes del código. Asegúrese de entender qué hace (recuerde que para esto no debería ser necesario mirar el código).
2 Recorriendo el laberinto
Complete el código para que el personaje se mueva a lo largo del laberinto, de acuerdo a las flechas del teclado, sin pasar por encima de las paredes ni salir fuera de escena.
Al inicio incluya las definiciones de constantes globales:
(require "extras.rkt") ; Definiciones principales: ; ======================== (define LABERINTO (bitmap "laberinto.png")) (define ALTO (image-height LABERINTO)) (define ANCHO (image-width LABERINTO)) (define CENTRO (make-posn (/ ANCHO 2) (/ ALTO 2))) (define POS-INICIAL (make-posn 15 35)) Luego, diseñe las funciones asociadas a la pantalla:
Incluiremos una función auxiliar: dibujar-imagen, que dibujará la imagen dada en la posición indicada por la estructura posn.
A lo largo del trabajo incluiremos las funciones dibujar-... que dibujaran cada uno de los componentes del juego.
La función grafica, unirá todos estos componentes para generar la imagen final, y se suscribirá al evento on-draw.
; Imprimir en la pantalla: ; ======================= ; dibujar-imagen : Imagen Posn Imagen -> Imagen ; .................................. ; dibujar-laberinto : Imagen -> Imagen ; .................................. ; dibujar-jugador : Estado Imagen -> Imagen ; .................................. ; grafica : Estado -> Imagen ; .................................. Para modificar la posición del jugador, diseñe 4 funciones auxiliares: move-up , move-down , move-right , move-left. Dada una posición y distancia, se desplazarán hacía cada dirección.
; Mover objetos: ; ============= ; move-up : Posn Number -> Posn ; .................................. ; move-down : Posn Number -> Posn ; .................................. ; move-left : Posn Number -> Posn ; .................................. ; move-right : Posn Number -> Posn ; .................................. Diseñe la función dentro-escena? que determine si la imagen dada en la posición dada, cabe dentro de la escena.
Utilizando la función anterior y la función interseca?, diseñe la función posible-pos? que determine si la imagen dada se puede dibujar en la posición dada, es decir, si cabe dentro de la escena y no se interseca con el laberinto.
; Posiciones: ; ========== ; dentro-escena? : Posn Imagen -> Boolean ; .................................. ; posible-pos? : Posn Imagen -> Boolean ; .................................. Diseñe una función manejador-tecla que se suscriba a los eventos del teclado y actualice la posición del jugador, siempre que sea posible.
; Eventos del teclado: ; =================== ; manejador-tecla : Estado Tecla -> Estado ; .................................. ; estado-inicial : Estado ; .................................. ; (big-bang estado-inicial ; [to-draw grafica] ; [on-key manejador-tecla])
3 Alcanzando el objetivo
Modifique el código para que se dibuje la posición objetivo, y se detecte y termine el programa cuando el jugador alcanza dicha posición (utilizar la función interseca?).
(define POS-OBJETIVO (make-posn 930 460))
Diseñe una función que dada la imagen de la escena, dibuje el objetivo sobre ella, llamada "dibujar-objetivo".
; Imprimir en la pantalla: ; ======================= ; .... ; dibujar-objetivo : Imagen -> Imagen ; .................................. - Además, para detectar si el jugador ha alcanzado el objetivo, diseñe una función "objetivo?", la cual se asociará a la cláusula stop-when.
; Condiciones de Terminación: ; ========================== ; objetivo? : Estado -> Boolean ; ..................................
4 El tiempo pasa
En este punto, ya no alcanzará con llevar registro de la posición del jugador en el estado, además, debemos registrar el tiempo disponible. Debemos recurrir a una estructura con diferentes campos.
Cuando trabajamos con una estructura compleja, es decir, con muchos campos, para no tener que enumerar todos los campos cada vez que se modifica uno solo, podemos definirnos algunas funciones auxiliares que nos permitan actualizar un campo en particular, dejando el resto intacto. Por ejemplo, supongamos definimos el estado del juego como:(define-struct estado [campo1 campo2 campo3 ...])
Y en varias oportunidades vamos a modificar únicamente el campo1, entonces definimos:(define (actualizar-campo1 val st) (make-estado val (estado-campo2 st) (estado-campo3 st) ...))
Y luego, utilizamos la función actualizar-campo1 en el resto del código.Siguiendo este enfoque, diseñe las funciones: actualizar-timer y actualizar-jugador.; Estado global del programa: ; ========================== ; (define-struct estado ....) ; actualizar-timer : Number Estado -> Estado ; .................................. ; actualizar-jugador : Posn Estado -> Estado ; .................................. - Diseñe la función dibujar-timer que dibuje el tiempo restante en la esquina inferior derecha.
; Imprimir en la pantalla: ; ======================= ; .... ; dibujar-timer : Estado Imagen -> Imagen ; .................................. - Además, debemos ir actualizando el paso del tiempo con cada tick del reloj. Para esto, defina la función manejador-tick que se suscriba al evento on-tick.
; Otros handlers: ; ============== ; manejador-tick : Estado -> Estado ; .................................. - Nuestro programa tendrá dos posibles condiciones para terminar, cuando se alcanza el objetivo, o cuando se acaba el tiempo. Defina la función fin? que analice ambas posibles condiciones (se asociará a la cláusula stop-when).
; Condiciones de Terminación: ; ========================== ; ... ; fin? : Estado -> Boolean ; ..................................
5 Color de fondo y mensajes
Al inicio debe ser blanco.
Cuando quedan menos de 500 ticks debe ser rojo.
Cuando se pierde, debe ser púrpura.
Cuando se gana, debe ser dorado.
Además, al lograr el objetivo, se debe imprimir el mensaje "GANASTE!", y al perder: "GAME OVER!".
- Diseñe una función dibujar-texto que dibuje el texto dado, centrado en la imagen.
; Imprimir en la pantalla: ; ======================= ; .... ; dibujar-texto : String Imagen -> Imagen ; .................................. - Una función fondo, que dado un color, genere una imagen de fondo del color dado.
; fondo : Color -> Imagen ; .................................. - Una función imprimir, que dado un color, dibuje toda la escena y sus componentes, con el fondo del color dado.
; imprimir : Estado Color -> Imagen ; .................................. - Utilizando las funciones anteriores, modifique la función grafica para que considere las diferentes condiciones del estado y de acuerdo a esto, genere la imagen con su color y texto adecuado.
; grafica : Estado -> Imagen ; ..................................
6 Opcional: Fantasmas.
Modifique el código para incluir un nuevo elemento a la escena, un fantasma que cae verticalmente a un cierto paso por tick, iniciando siempre en una posición aleatoria del tope de la escena. Además de mover al fantasma, se debe detectar cuando interseca con el jugador (utilizando interseca?) y en dicho caso terminar el programa (con el mensaje "GAME OVER!").
(define FANTASMA (bitmap "fantasma.png"))
- Siguiendo el ejemplo de los pasos anteriores, incorpore las funciones actualizar-fantasma que permita actualizar la posición del fantasma en el estado, y la función dibujar-fantasma que grafique el fantasma en la posición adecuada.
; Estado global del programa: ; ========================== ; .... ; actualizar-fantasma : Posn Estado -> Estado ; .................................. ; Imprimir en la pantalla: ; ======================= ; ... ; dibujar-fantasma : Estado Imagen -> Imagen ; .................................. - Diseñe una función mover-fantasma que actualice la posición del fantasma, e incorpórela en la definición de la función manejador-tick para que se actualice la posición del fantasma en cada tick.
; Mover objetos: ; ============= ; ... ; mover-fantasma : Posn -> Posn ; .................................. - Diseñe una función interseca-fantasma? que determine si el jugador y el fantasma se intersecan en el estado dado (utilizar la función interseca?). Utilícela para actualizar las condiciones de terminación del programa.
; Condiciones de Terminación: ; ========================== ; ..... ; interseca-fantasma? : Estado -> Boolean ; ..................................
7 Opcional: Vidas.
Modifique el código para que se lleve el registro de vidas disponibles (por ejemplo 5) Y cada vez que el jugador interseca con el fantasma, se reinicie de la posición inicial, con una vida menos pero sin reiniciar el timer. Si las vidas se acaban el programa debe terminar.
Las vidas restantes se debe mostrar en la esquina inferior izquierda.
; Estado global del programa: ; ========================== ; ... ; actualizar-vidas : Number Estado -> Estado ; .................................. ; Imprimir en la pantalla: ; ======================= ; ... ; dibujar-vidas : Estado Imagen -> Imagen ; ..................................
8 Diversos laberintos
Una vez terminado el diseño del programa, puede probarlos con diferentes laberintos, por ejemplo las imágenes "laberinto2.png" y "laberinto3.png". Si el programa está adecuadamente implementado, no debería modificar ningún campo, excepto POS-INICIAL y POS-OBJETIVO, que determinan la posición inicial y el objetivo a alcanzar.
9 Opcional: Un fantasma más inteligente
Programar el comportamiento del fantasma para que recorra aleatoriamente el laberinto sin pasar por encima de las paredes.