Tareas

De Wiki del curso de C
Saltar a: navegación, buscar

Tarea 1: Versión inicial del mundo

  1. Haz un fork del repositorio con tu nombre
  2. Clona tu repositorio
  3. Completa el esqueleto proporcionado para implementar una primera versión funcional del juego de la vida
  4. Sube los cambios al tu repositorio
  5. Haz un pull request
  6. Arregla las correcciones del profesor
  7. Sube las correcciones a tu repo
  8. ¿Has conseguido que te acepten el pull request?
    1. NO -> goto 6
    2. Sí -> ¡Enhorabuena! Ya has terminado la tarea 1

Tarea 2: Código modular, estructuras y makefile

a) Divide tu programa en 3 fichero:

  • main.c : Implementará el bucle principal del juego
  • gol.h  : Tendrá las declaraciones de las funciones relacionadas con el juego de la vida
  • gol.c  : Tendrá las definiciones de las funciones anteriores

b) Crea un makefile para gestionar la compilación y dependencias

Tarea 3: Primera aproximación a objetos

1. Encapsula tu mundo en la siguiente estructura:

struct world {
        bool w1[W_SIZE_X][W_SIZE_Y];
        bool w2[W_SIZE_X][W_SIZE_Y];
};

2. Modifica tus funciones para que reciban un puntero a tu objeto. Añade el modificador const siempre que no sea necesario modificar el objeto

Tu main.c debería quedar más o menos así:

#include <stdio.h>
#include <stdlib.h>
#include "gol.h"

int main()
{
	int i = 0;
	struct world w;

	world_init(&w);
	do {
		printf("\033cIteration %d\n", i++);
		world_print(&w);
		world_step(&w);
	} while (getchar() != 'q');

	return EXIT_SUCCESS;
}

Tarea 4: Objetos: Reserva dinámica de memoria

Interfaz pública

gol.h

struct world;

struct world *world_alloc(int size_x, int size_y);
void world_free(struct world *w);

void world_print(const struct world *w);
void world_iterate(struct world *w);
  • world_alloc: Reserva en memoria e inicializa nuestro objeto struct world. Esto implica reservar memoria para la estrucutra y para los dos arrays que tiene dentro.
  • world_free: Libera la memoria que ocupa nuestro objeto. Esto implica liberar primero la memoria de los arrays que tiene dentro la estrucutra, y luego la propia estructura.
  • world_print: Imprime el mundo
  • world_iterate: Realiza una iteración del juego de la vida


Implementación (privada)

gol.c

struct world
{
	bool *cells[2];
	int size_x;
	int size_y;
};

static void fix_coords(const struct world *w, int *x, int *y);
static bool get_cell(const struct world *w, int x, int y);
static void set_cell(struct world *w, int buf, int x, int y, bool val);
static int count_neighbors(const struct world *w, int x, int y);

/* ... */
/* Definiciones de funciones privadas y públicas */
/* ... */
  • fix_coords: Recibe unas coordenadas (x,y) y las modifica para implementar un mundo toroidal
  • get_cell: Devuelve la célula en la posición (x,y), arreglando las coordenadas.
  • set_cell: Cambia el valor de la célula de la posición (x,y), arreglando las coordenadas.
  • count_neighbors: Cuenta las células vecinas haciendo uso de la función get_cell

NOTAS

  • No olvides comprobar que has podido realizar correctamente la reserva de memoria:

En world_alloc():

w = (struct world *)malloc(sizeof(struct world));
if (!w)
	return NULL;

En main():

w = world_alloc(WORLD_X, WORLD_Y);
if (!w) {
	perror("Can't allocate world");
	exit(EXIT_FAILURE);
}
  • Las funciones privadas se declaran como static y no aparecen el el .h
  • Ahora debes calcular a mano el offset en el array para llegar a la célula (x,y) con la fórmula: size_x*y + x. Encapsula esta fórmula en las funciones get_cell y set_cell.

Tarea 5: Argumentos y lectura de ficheros

Al final de esta tarea tu juego de la vida deberá poder configurarse tanto por paso de argumentos como mediante un archivo de configuración. La ayuda del programa devolvería lo siguiente:

Usage: gol
	[-h|--help]
	[-x|--size_x <num>]
	[-y|--size_y <num>]
	[-i|--init <init_mode>]
	[config_file]


Esta tarea se divide en 3 subtareas distintas que deben corresponder con 3 commits, uno por subtarea. La subtarea 3 es **opcional**.

Tarea 5.1: Objeto struct config

Completa la plantilla que encontrarás aquí: Archivo:Cfg template.tar para agrupar toda la lógica de configuración en un objeto.

Intégrala después en tu main de esta forma (debes modificar también world_alloc):

 1 int main(int argc, char *argv[])
 2 {
 3 	struct config config;
 4 	struct world * w;
 5 
 6 	if (!config_parse_argv(&config, argc, argv)) {
 7 		printf("ERROR\n");
 8 		config_print_usage(argv[0]);
 9 		return EXIT_FAILURE;
10 	} else if (config.show_help){
11 		config_print_usage(argv[0]);
12 		return EXIT_SUCCESS;
13 	}
14 
15 	w = world_alloc(&config);
16 	if (!w) {
17 		perror("Can't allocate world");
18 		exit(EXIT_FAILURE);
19 	}
20 
21 	do {
22 		printf("\033cIteration %d\n", i++);
23 		world_print(w);
24 		world_iterate(w);
25 	} while (getchar() != 'q');
26 
27 	world_free(w);
28 	return EXIT_SUCCESS;
29 }

De esta plantilla hay que destacar los siguientes puntos:

  • Se define un objeto sencillo, público, sin reserva dinámica de memoria
  • Se usa un enumerado para definir el tipo de inicialización con las siguientes peculiaridades:
    • El primer elemento representa un modo no válido y forzamos su valor a -1
    • El último elemento se usa para tener en una macro el número de elementos del enumerado. Por ello el nombre está entre barras bajas, para marcarlo como una opción no válida
    • En el .c se define un array constante de cadenas, asociando cada cadena con su correspondiente valor en el enumerado. Se utiliza un inicializador un tanto particular, dónde inicializamos explicitamente los elementos del array mediante su índice: El índice se indica entre corchetes y se le asigna un valor con el operador igual: [3] = 41
    • Se usa el array anterior para convertir el enumerado a una cadena, simplemente hay que acceder al array con el valor del enumerado como índice: init_mode_str[CFG_GLIDER]
  • Se define una función privada check_config que agrupa la lógica de validación de argumentos. Devuelve false si los argumentos nos son válidos y true si lo son
  • La función str2init_mode recibe una cadena y la va comparando una por una con las cadenas de modos de inicialización para ver con cual coincide. Si no coincide con ninguna devuelve CFG_NOT_DEF para indicar que no es un modo válido.

Tarea 5.2: Cargar fichero de configuración

Ahora has de permitir que tu programa lea la configuración de un archivo:

  • El nombre del archivo ha de recibirse a través de los argumentos del programa, pero **no a través de un flag con getopt**. Ejemplo: > gol config.txt
  • Las configuraciones del archivo tienen preferencia sobre los argumentos por consola
  • El archivo de configuración constará de 3 líneas:
    1. Ancho del mundo
    2. Alto del mundo
    3. Tipo de inicialización

Ejemplo config.txt:

20
15
glider
  • Crea una función privada static bool load_config(struct config *config); que se encarge de abrir, parsear y cerrar el fichero. Devolverá cierto si todo ha ido bien y falso en caso contrario.
  • La función fgets devuelve una cadena con un salto de línea (si lo hay). Vas a tener que quitárselo haciendo uso de la función strchr
  • Échale un ojo a este fragmento de código:
 1 int config_parse_argv(struct config *config, int argc, char *argv[])
 2 {
 3 	...
 4 
 5 	// Check for config file name
 6 	if (optind != argc) {
 7 		if (optind == argc - 1) {
 8 			config->cfg_file = argv[optind];
 9 			if (!load_config(config))
10 				return false;
11 		} else {
12 			return false;
13 		}
14 	}
15 
16 	return check_config(config);
17 }
  • Y a este otro:
1 	// Size x
2 	char line[LINE_LEN];
3 	fgets(line, LINE_LEN, file);
4 	if (ferror(file)) {
5 		perror("Error reading config file");
6 		return false;
7 	}
8 	config->size_x = strtol(line, NULL, 0);

Tarea 5.3 [opcional]: Guarda y carga de estado

Haz que tu juego de la vida guarde el estado del mundo antes de salir, en un archivo con el nombre que elijas. Haz también que este archivo se pueda cargar al lanzarlo de nuevo, para que comience por el mismo estado.

Notas:

  • Has de crear dos nuevas opciones getopt para indicar el nombre del archivo a guardar y a cargar
  • Para guardar el estado, haz un volcado directo en binario del array en un archivo. Utiliza la función fwrite pasándole tu array y su tamaño. Ten en cuenta que debes guardar también las dimensiones de tu mundo.
  • Para cargar el estado, haz una lectura directa del archivo binario con la función fread. Puedes volcar el contenido del archivo directamente en el array de tu estructura, pero no olvides reservar la memoria primero (para esto debes leer antes las dimensiones de tu mundo)

Tarea 6: Herencia

  • Divide tu arquitectura en tres objetos:
    • world: Objeto abstracto que no tienen implementada las funciones world_get_cell y world_set_cell, pero sí el resto
    • world_limited: Objeto que hereda de world e implementa las funciones world_get_cell y world_set_cell de forma que se devuelva una célula muerta si los índices se salen del array y no haga nada en el caso del setter
    • world_toroidal: Objeto que hereda de world e implementa las funciones world_get_cell y world_set_cell para accedar al mundo de forma toroidal. Aquí además debes implementar la función fix_coords para usarla en tu getter y setter.

Herencia.png

NOTA: Ejercicio de ejemplo resuelto: Archivo:Herencia res.tar

Tarea 7: Listas

Modifica tu mundo para utilizar la doble estructura (array + lista) que se ha explicado en clase. Repasar descargar

  • Debes modificar tus tres objetos (world, world_limited y world_toroidal), aunque la mayor parte de los cambios van en el objeto padre, mientras que en los hijos solo debes cambiar la forma en la que accedes al array (puesto que ahora habrá solo un array).
  • Creamos un objeto nuevo, struct cell, muy sencillo y con todos sus atributos públicos. Este objeto será el elemento de nuestras listas.
  • Ten en consideración las siguientes pistas y consulta esta plantilla: Archivo:gol_list.tar


Estructura celula

Guarda un par de coordenadas para identificarla y facilitarnos el acceso al array

struct cell {
	int x,y;
	struct list_head lh;
};

Estructura del objeto padre

struct world
{
	bool *cells; // Observa que ahora solo necesitamos un array
	int size_x;
	int size_y;
	// TODO: Crea las cabezas de lista (tipo `struct list_head`)
};

Constructor objeto padre

  • A la hora de reservar espacio para el array, lo hacemos solo una vez y guardamos la dirección en w->cells
  • A la hora de inicializar nuestro array revivir cada célula en dos pasos:
    1. Crear el nodo de lista (tipo struct cell) y comprobar que se ha realizado correctamente la reserva de memoria
    2. Establecer a cierto la celula en el array

Esto lo podemos realizar a mano o creando una función auxiliar, digamos add_initial_cell

Función count_neighbors

Para facilitar la implementación de la nueva función world_step, os propongo esta mejora para contar vecionos.

  1. Creamos un array estático con las 8 sumas/restas que hay que aplicar a unas coordenadas (x,y) para obtener sus 8 vecinas
  2. Recorremos este array sumando estas diferencias para obtener las coordenadas de las células vecinas
  3. Usamos siempre nuestro método get_cell (implementado por los hijos) para que tenga en cuenta los límites del mundo
static const int neighbors_coords[8][2] = {
	{-1, -1}, {0, -1}, {1, -1},
	{-1,  0},          {1,  0},
	{-1,  1}, {0,  1}, {1,  1}
};

static int count_neighbors(const struct world *w, int x, int y)
{
	int count = 0;

	for (int i=0; i < 8; i++)
		count += get_cell(w,
			x + neighbors_coords[i][0],
			y + neighbors_coords[i][1]);

	return count;
}

Función fix_coord

Cambiamos el prototipo de esta función para que devuelva un booleano indicando si las coordenadas que ha recibido son validas o no. Esto nos hará falta posteriormente en la función world_step.

  • En un mundo **toroidal** unas coordenadas fuera del mundo, ej (-1, TAM_Y), serán corregidas, por lo fix_coord debe devolver siempre **cierto**
  • En un mundo **limitado** unas coordenadas fuera del mundo, ej (-1, TAM_Y) son inválidas, por lo que fix_coord debe devolver **falso** en estos casos.

Función world_step

  • Consultar la plantilla para tener una idea de cómo implementar esta función
  • Fijaos que, cuado estamos comprobando las células vecinas, tenemos que tener especial cuidado con las que caen fuera de nuestro mundo:
    1. En un mundo **toroidal** corregimos las coordenadas y aplicamos el algoritmo normal. Debemos corregirlas para no crear un nodo en nuestra lista con coordenadas inválidas.
    2. En un mondo **limitado** no tiene sentido añadir a nuestra lista nodos con coordenadas fuera del array, puesto que estas células limítrofes siempre están muertas. Por ello, si fix_coords nos devuelve falso, no procesamos esa célula.

Tarea 8: Iterfaz Gráfica

Implementa una interfaz gráfica para tu mundo con GTK. Sigue este código de ejemplo: Archivo:gol_gui.tar