Práctica 4 (30%) | P2 GIR Saltearse al contenido

Práctica 4 (30%)

A continuación se encuentra el enunciado de la práctica 4. Lee cuidadosamente el enunciado y sigue las instrucciones. Se recomienda consultar el apartado de teoría para tratar de resolver dudas de concepto.


1 Cambios

1.1 Versión 1.0.0

  • Enunciado original.

2 Archivos de partida

Aquí se encuentran los archivos de partida. Descárgalos y utilízalos como base para resolver la práctica.

Los archivos de partida contienen una estructura básica de proyecto, con el Makefile, el programa principal de prueba y los ficheros .h, .cc y .tcc que deberás completar. En esta práctica, también debes desarrollar los .h.

Los archivos se encontrarán dentro de una carpeta llamada irp2-p4, y a su vez, los archivos de cabecera y de implementación estarán en una subcarpeta llamada src.

La estructura inicial será:

irp2-p4
├── Makefile
├── check.cc
├── mainp4.cc
├── uml
| ├── uml.png
| └── uml_simple.png
└── src
├── color.h
├── pieza.h
├── pieza.cc
├── fichanormal.h
├── fichanormal.cc
├── fichadama.h
├── fichadama.cc
├── contenedor.h
├── contenedor.tcc
├── tablero.h
└── tablero.cc

Los archivos de partida incluyen todos estos archivos ya creados en src, pero vacíos, con solo algunos elementos básicos como la guarda de los ficheros y comentarios para añadir el nombre y el DNI.


3 Objetivos

El objetivo principal de esta práctica es aplicar los primeros conceptos de Programación Orientada a Objetos vistos en clase.

En esta práctica trabajaremos:

  • Clases y objetos.
  • Herencia.
  • Clases abstractas.
  • Enlace dinámico.
  • Relaciones entre clases (composición).
  • Clases genéricas con plantillas (template).
  • Sobrecarga de operadores (operator<< y operator[]).
  • Funciones amigas (friend).
  • Modularización del código en archivos .h, .cc y .tcc.
  • Gestión básica de memoria dinámica.

También verás que el proyecto está preparado para separar en diferentes carpetas (src, bin) el código fuente y los archivos generados tras la compilación. Para ello, puedes fijarte en el archivo Makefile. Cuando utilices la herramienta make, los archivos generados tras la compilación se guardarán en una carpeta llamada bin. Esta carpeta no es necesario que exista previamente, ya que se crea automáticamente si no existe.


4 Enunciado

Primero se va a explicar en términos generales la idea del proyecto y algunos conceptos básicos de UML. Después describiremos las funciones a implementar.

4.1 Introducción

En esta práctica vas a implementar una versión simplificada del juego de las damas utilizando C++.

El programa deberá representar:

  • Un tablero de 8x8.
  • Fichas para dos jugadores (blancas y negras).
  • Fichas normales.
  • Fichas dama.
  • El turno actual.
  • Un historial de movimientos.

El objetivo no es implementar todas las reglas oficiales de las damas, sino construir una pequeña aplicación orientada a objetos que permita practicar los conceptos vistos en clase.


4.2 Reglas simplificadas del juego

El tablero tendrá tamaño 8x8.

Las filas y columnas se representarán internamente con números enteros entre 0 y 7.

  • La fila 0 representa la parte superior del tablero.
  • La fila 7 representa la parte inferior del tablero.
  • La columna 0 representa la columna a en el tablero típico.
  • La columna 7 representa la columna h en el tablero típico.

Para mostrar movimientos en el historial se usará una notación similar a la del ajedrez. Por ejemplo: a1, h8, c3, b6.

En esta notación se escribe primero la columna y después la fila:

  • La columna se representa con una letra de la a a la h, de izquierda a derecha en el tablero.
  • La fila se representa con un número del 1 al 8, de abajo hacia arriba en el tablero.

Fíjate en los siguientes ejemplos, convirtiendo los valores internos en la notación mencionada:

fila 7, columna 0 -> a1
fila 0, columna 7 -> h8
fila 5, columna 2 -> c3
fila 2, columna 1 -> b6

Esta información la necesitarás para implementar algunas partes de la práctica.


4.3 Colocación inicial de las fichas

Al inicializar el tablero se colocarán fichas normales en las casillas oscuras (ten en cuenta que la esquina inferior izquierda a1 es oscura), y los colores se intercalan a lo largo de todo el tablero.

La colocación inicial será:

  • Fichas negras en las filas 8, 7 y 6 según la notación descrita previamente, que corresponden a las filas internas 0, 1 y 2.
  • Fichas blancas en las filas 1, 2 y 3 según la notación descrita previamente, que corresponden a las filas internas 7, 6 y 5.

Todas las fichas colocadas inicialmente serán fichas normales.

Visualizando el tablero, se quedaría así tras colocar las piezas iniciales (o para blancas, x para negras):

a b c d e f g h
8 [ ][x][ ][x][ ][x][ ][x] 8
7 [x][ ][x][ ][x][ ][x][ ] 7
6 [ ][x][ ][x][ ][x][ ][x] 6
5 [ ][ ][ ][ ][ ][ ][ ][ ] 5
4 [ ][ ][ ][ ][ ][ ][ ][ ] 4
3 [o][ ][o][ ][o][ ][o][ ] 3
2 [ ][o][ ][o][ ][o][ ][o] 2
1 [o][ ][o][ ][o][ ][o][ ] 1
a b c d e f g h

4.4 Movimiento de las fichas

En esta práctica se separan dos responsabilidades:

  • Las piezas conocen la forma en la que se pueden mover y su posición actual.
  • El tablero conoce el estado de las casillas y decide si un movimiento puede realizarse en el tablero.

Por tanto, las clases FichaNormal y FichaDama solo comprobarán si la forma del movimiento es válida para su tipo de pieza.

Las comprobaciones relacionadas con el estado del tablero se harán en el método mover de la clase Tablero.


Ficha normal

Una ficha normal:

  • Se mueve una casilla en diagonal hacia delante.
  • Las fichas blancas avanzan hacia filas internas menores, es decir, visualmente hacia la parte superior del tablero.
  • Las fichas negras avanzan hacia filas internas mayores, es decir, visualmente hacia la parte inferior del tablero.

La ficha normal solo comprobará si la forma del movimiento es correcta.

La comprobación de si la casilla de destino está vacía será responsabilidad de la clase Tablero.

Ejemplo de movimiento simple de una ficha blanca:

c3 -> d4

Para simplificar el desarrollo de la práctica, no se implementarán capturas.


Ficha dama

Una ficha dama:

  • Se mueve una casilla en diagonal en cualquier dirección.

La dama solo comprobará si la forma del movimiento es correcta.

La comprobación de si la casilla de destino está vacía será responsabilidad de la clase Tablero.

Para simplificar la práctica, la ficha dama no se moverá varias casillas en diagonal. Solo podrá moverse una casilla en diagonal.

Este tipo de fichas tampoco implementarán capturas.


4.5 Promoción a dama

Cuando una ficha normal llega al extremo opuesto del tablero, se convierte automáticamente en dama.

  • Una ficha blanca promociona al llegar a la fila 8 según la notación del tablero, que corresponde a la fila interna 0.
  • Una ficha negra promociona al llegar a la fila 1 según la notación del tablero, que corresponde a la fila interna 7.

La promoción se realizará después de mover correctamente la ficha.

Para promocionar una ficha, el tablero deberá sustituir la FichaNormal por una nueva FichaDama del mismo color y en la misma posición.

Una vez se realiza la promoción, el turno pasará al otro jugador.

No hay que registrar la promoción en el historial, pero sí el movimiento que se ha realizado como cualquier otro movimiento.


4.6 Reglas no implementadas

Para mantener la práctica sencilla, no se implementarán las siguientes reglas:

  • Capturas o capturas múltiples.
  • Detección automática de ganador.
  • Detección de empate.
  • Fin automático de la partida.
  • Entrada interactiva por teclado (esto lo puedes hacer en el main para simularlo, pero no forma parte del código evaluable).

Por lo tanto, cíñete a lo que se describe en el enunciado. Aunque te sepas las reglas del juego, no estamos programando el juego completo.


5 Diagramas UML

A continuación se muestran dos diagramas UML que representan las clases principales de la práctica y sus relaciones.

El primer diagrama muestra la estructura completa de las clases, incluyendo atributos, funciones y relaciones.

Diagrama de clases UML

Figura 1: Diagrama de clases UML completo que representa la estructura del proyecto que tienes que desarrollar.

El segundo diagrama muestra una versión simplificada, centrada únicamente en las relaciones entre clases. Este diagrama puede ayudarte a entender mejor qué clases heredan de otras, qué clases contienen objetos de otras clases y qué clases simplemente usan otras clases. Sin embargo, ten en cuenta que lo habitual es trabajar con el UML completo.

Diagrama simplificado de relaciones UML

Figura 2: Diagrama UML simplificado con las relaciones principales entre las clases.


6 Cómo interpretar el UML

En el diagrama UML aparecen distintos tipos de relaciones.

6.1 Herencia

En UML, la herencia se representa mediante una línea con una flecha triangular hueca.

La flecha apunta hacia la clase base.

En esta práctica, las flechas de herencia apuntan hacia Pieza, porque Pieza es la clase base.

Esto significa que:

  • FichaNormal hereda de Pieza.
  • FichaDama hereda de Pieza.

La clase Pieza será una clase abstracta. Las clases FichaNormal y FichaDama deberán proporcionar su propia implementación de los métodos get_nombre y es_movimiento_valido.

En esta práctica trabajaremos con herencia pública.


6.2 Relación de uso

En UML, una relación de uso se suele representar mediante una línea discontinua con una flecha.

Indica que una clase utiliza otra clase de forma temporal, pero no la contiene como atributo.

Por ejemplo, en esta práctica la clase Tablero usa las clases FichaNormal y FichaDama para crear piezas.

El tablero crea fichas normales al inicializar la partida.

También crea fichas dama cuando una ficha normal promociona.

Esta relación no significa que Tablero tenga un atributo de tipo FichaNormal o FichaDama.


6.3 Composición y cardinalidad

En UML, la composición se representa mediante una línea con un rombo negro en uno de sus extremos.

El rombo negro se coloca junto a la clase que contiene a la otra.

En esta práctica, el rombo negro aparece junto a la clase Tablero, porque el tablero contiene otros objetos.

Por ejemplo, el Tablero contiene piezas.

Esto significa que:

  • El Tablero tiene piezas.
  • Las piezas forman parte del tablero.
  • El tablero es responsable de liberar la memoria de las piezas que sigan colocadas en él cuando se destruya.

Junto a la relación también aparecen números, como 1 y 0..64.

Esos números indican la cardinalidad o multiplicidad de la relación.

En este caso:

  • El 1 junto a Tablero indica que estamos hablando de un tablero.
  • El 0..64 junto a Pieza indica que ese tablero puede contener desde 0 hasta 64 piezas.

En C++, esta relación se implementará mediante el atributo privado casillas:

Pieza* casillas[8][8];

Aunque en una partida inicial de damas no haya 64 piezas, el tablero tiene 64 casillas y, por tanto, puede contener como máximo una pieza por casilla. Considera la posibilidad de que el tablero pueda ser utilizado para otros juegos diferentes a las damas. Con un diseño adecuado, es posible extender el proyecto a otros juegos sin demasiada complicación. Piensa cómo harías que este programa funcione para ajedrez, o para Reversi.


El Tablero también contiene un historial de movimientos.

En el diagrama, esta relación también aparece con un rombo negro junto a Tablero.

Esto significa que el historial forma parte del tablero.

En C++, esta relación se implementará mediante el atributo privado historial:

Contenedor<std::string, 50> historial;

En este caso, la cardinalidad indica que cada Tablero contiene exactamente un Contenedor.

Ese contenedor almacenará cadenas de texto con los movimientos realizados durante la partida.


6.4 Visibilidad

En UML se utilizarán los siguientes símbolos de visibilidad:

+ público
- privado

Por ejemplo:

-simbolo: char

indica que simbolo es un atributo privado de tipo char.

+get_simbolo(): char

indica que get_simbolo es un método público que devuelve un dato de tipo char.


6.5 Funciones amigas

En esta práctica se utilizarán funciones amigas mediante la palabra reservada friend.

Los operadores de salida operator<< deberán implementarse como funciones amigas de las clases correspondientes.

Esto permitirá que el operador pueda acceder directamente a los atributos privados de la clase.

Por ejemplo:

friend std::ostream& operator<<(std::ostream& os, const Pieza& pieza);

La palabra friend solo se escribe en la declaración de la función amiga dentro de la clase.

No se escribe friend en la implementación de la función.

Por ejemplo, en pieza.cc, la implementación empezará así:

std::ostream& operator<<(std::ostream& os, const Pieza& pieza) {
// ...
}

En el caso de la clase genérica Contenedor<T, CAPACIDAD>, al tratarse de una plantilla, la implementación del operador de salida estará en los archivos de cabecera, normalmente en contenedor.tcc, pero se aplica la misma idea: friend se escribe en la declaración dentro de la clase, no en la implementación.


7 Clases y otros tipos

A continuación se describen las clases que debes implementar junto al enumerado Color.


7.1 Enumerado Color

El archivo color.h contendrá un tipo enumerado llamado Color.

Este tipo representa un color del juego. Se utilizará en dos contextos:

  • Como atributo de cada pieza, para indicar si la pieza es blanca o negra.
  • Como atributo del tablero, para indicar qué color tiene el turno actual.

Es decir, Color no es una clase que gestione el turno. Simplemente es el tipo de dato usado por el atributo turno de la clase Tablero.

Valores

ValorDescripción
BLANCORepresenta las fichas blancas.
NEGRORepresenta las fichas negras.

El enumerado tendrá la siguiente forma:


7.2 Clase Pieza

La clase Pieza representa una pieza genérica del tablero.

Será la clase base de FichaNormal y FichaDama.

La clase Pieza será una clase abstracta. Las clases derivadas deberán proporcionar su propia implementación de los métodos get_nombre y es_movimiento_valido.

Atributos privados

AtributoTipoDescripción
simbolocharCarácter usado para representar la pieza en el tablero.
colorColorColor de la pieza.
filaintFila actual de la pieza.
columnaintColumna actual de la pieza.

Funciones

FunciónParámetrosDevuelveDescripción
Piezachar simbolo, Color color, int fila, int columnaNadaConstructor. Inicializa los atributos de la pieza.
~PiezaNingunoNadaDestructor virtual. Permite destruir correctamente objetos derivados a través de punteros a Pieza.
get_simboloNingunocharDevuelve el símbolo de la pieza.
get_colorNingunoColorDevuelve el color de la pieza.
get_filaNingunointDevuelve la fila actual de la pieza.
get_columnaNingunointDevuelve la columna actual de la pieza.
set_posicionint fila, int columnavoidCambia la posición interna de la pieza.
get_nombreNingunostd::stringDevuelve el nombre de la pieza.
es_movimiento_validoint fila_destino, int columna_destinoboolIndica si la forma del movimiento es válida para ese tipo de pieza.

Nota: Piensa por qué la clase Pieza tiene un destructor virtual (esto no se suele indicar en el diagrama UML).

Los métodos que no modifican el estado de la pieza deberán declararse como constantes. En el diagrama UML se indica cuáles son con el modificador << const >>

El método es_movimiento_valido solo comprueba la regla de movimiento propia de cada tipo de pieza:

  • Una ficha normal se mueve una casilla en diagonal hacia delante.
  • Una dama se mueve una casilla en diagonal en cualquier dirección.

Las comprobaciones relacionadas con el estado del tablero se realizarán en Tablero::mover.

Operador de salida

También se deberá implementar el operador de salida para poder mostrar una pieza por pantalla.

El operador de salida será una función amiga de la clase Pieza.

FunciónParámetrosDevuelveDescripción
operator<<std::ostream& os, const Pieza& piezastd::ostream&Función amiga que añade al flujo de salida la representación de la pieza.

La declaración dentro de la clase será:

friend std::ostream& operator<<(std::ostream& os, const Pieza& pieza);

Una pieza se mostrará entre corchetes:

[o]
[x]
[O]
[X]

La representación será:

o -> ficha blanca normal
x -> ficha negra normal
O -> ficha blanca dama
X -> ficha negra dama

7.3 Clase FichaNormal

La clase FichaNormal hereda de Pieza.

Representa una ficha normal del juego de damas.

Funciones

FunciónParámetrosDevuelveDescripción
FichaNormalColor color, int fila, int columnaNadaConstructor. Crea una ficha normal del color indicado en la posición indicada.
get_nombreNingunostd::stringDevuelve el nombre de la pieza.
es_movimiento_validoint fila_destino, int columna_destinoboolIndica si la forma del movimiento es válida para una ficha normal.

Constructor

El constructor deberá asignar automáticamente el símbolo correspondiente:

o -> ficha blanca normal
x -> ficha negra normal

Nombre de la pieza

El método get_nombre devolverá el texto: Ficha

Este texto representará el tipo de pieza y se utilizará, por ejemplo, para construir las entradas del historial de movimientos:

Ficha blanca: b7 -> a8

Movimiento

Una ficha normal blanca avanza hacia filas internas menores, es decir, hacia la parte superior del tablero.

Una ficha normal negra avanza hacia filas internas mayores, es decir, hacia la parte inferior del tablero.

El método es_movimiento_valido comprobará únicamente que la ficha normal se mueve una casilla en diagonal hacia delante.

No comprobará si la casilla de destino está vacía, ya que esa responsabilidad pertenece a Tablero.

En esta práctica no se implementan capturas.


7.4 Clase FichaDama

La clase FichaDama hereda de Pieza.

Representa una dama del juego de damas.

Aunque al inicializar el tablero todas las piezas serán fichas normales, las fichas dama aparecerán durante la partida cuando una ficha normal promocione al llegar al extremo opuesto del tablero.

Funciones

FunciónParámetrosDevuelveDescripción
FichaDamaColor color, int fila, int columnaNadaConstructor. Crea una dama del color indicado en la posición indicada.
get_nombreNingunostd::stringDevuelve el nombre de la pieza.
es_movimiento_validoint fila_destino, int columna_destinoboolIndica si la forma del movimiento es válida para una dama.

Constructor

El constructor deberá asignar automáticamente el símbolo correspondiente:

O -> ficha blanca dama
X -> ficha negra dama

Nombre de la pieza

El método get_nombre devolverá el texto: Dama

Movimiento

El método es_movimiento_valido comprobará únicamente que la dama se mueve una casilla en diagonal en cualquier dirección.

No comprobará si la casilla de destino está vacía, ya que esa responsabilidad pertenece a Tablero.

Para simplificar la práctica, la dama no se moverá varias casillas en diagonal.

En esta práctica no se implementan capturas.


7.5 Clase genérica Contenedor<T, CAPACIDAD>

La clase Contenedor será una clase genérica implementada con plantillas.

Tendrá dos parámetros de plantilla:

template <typename T, size_t CAPACIDAD>
  • T indica el tipo de dato que se almacena.
  • CAPACIDAD indica el número máximo de elementos que puede almacenar.

En esta práctica, el tablero usará el contenedor para guardar el historial de movimientos:

Contenedor<std::string, 50> historial;

Importante

No se debe usar std::vector dentro de Contenedor.

El contenedor deberá usar un array interno de tamaño fijo.

La implementación de una plantilla debe estar disponible en el momento de la compilación. Por este motivo, el archivo contenedor.h deberá incluir al final el archivo contenedor.tcc.

El archivo contenedor.tcc no se compila por separado. Debe considerarse como un archivo de cabecera auxiliar. Si usas make, esto ya lo tiene en cuenta.

Atributos privados

AtributoTipoDescripción
nombrestd::stringNombre del contenedor.
elementosT[CAPACIDAD]Array interno donde se almacenan los elementos.
num_elementossize_tNúmero actual de elementos almacenados.

Funciones

FunciónParámetrosDevuelveDescripción
Contenedorconst std::string& nombreNadaConstructor. Inicializa el nombre y deja el contenedor vacío.
set_nombreconst std::string& nombrevoidCambia el nombre del contenedor.
get_nombreNingunoconst std::string&Devuelve el nombre del contenedor.
agregarconst T& elementoboolAñade un elemento al final del contenedor si queda espacio.
operator[]size_t iT&Devuelve una referencia modificable al elemento situado en la posición i.
operator[]size_t iconst T&Devuelve una referencia constante al elemento situado en la posición i.
sizeNingunosize_tDevuelve el número de elementos almacenados.
capacidadNingunosize_tDevuelve la capacidad máxima del contenedor.
vacioNingunoboolDevuelve true si el contenedor no tiene elementos.
llenoNingunoboolDevuelve true si el contenedor ha alcanzado su capacidad máxima.

Los métodos que no modifican el estado del contenedor deberán declararse como constantes (ya indicados en el UML).

Funcionamiento de agregar

El método agregar añadirá un elemento al final del contenedor si queda espacio disponible.

Si el elemento se añade correctamente, devolverá true.

Si el contenedor está lleno, no añadirá el elemento y devolverá false.

Al ser un array automático, tendrás que hacer uso del atributo num_elementos para saber cuántos elementos válidos hay dentro del array.

Operador de acceso operator[]

La clase Contenedor<T, CAPACIDAD> deberá sobrecargar el operador [] para permitir acceder a los elementos almacenados mediante su posición.

Se implementarán dos versiones:

T& operator[](size_t i);
const T& operator[](size_t i) const;

Ejemplo de uso:

Contenedor<std::string, 50> historial("Historial");
historial.agregar("Ficha blanca: c3 -> d4");
std::cout << historial[0] << std::endl;

No será necesario comprobar si la posición i está dentro del rango válido. Se asumirá que quien usa el contenedor accede a posiciones correctas entre 0 y size() - 1. De esta manera, tendremos un comportamiento similar a un array estándar, en el que si ocurre esto, puede finalizar automáticamente el programa.

Operador de salida

También se deberá implementar el operador de salida para Contenedor.

El operador de salida será una función amiga de la clase Contenedor.

FunciónParámetrosDevuelveDescripción
operator<<std::ostream& os, const Contenedor<T, CAPACIDAD>& contenedorstd::ostream&Función amiga que añade al flujo de salida el contenido del contenedor.

Al tratarse de una plantilla, el operador de salida también deberá implementarse en los archivos de cabecera, no en un .cc.

Puede declararse como función amiga dentro de la clase Contenedor.

El operador deberá mostrar:

  1. El nombre del contenedor, si no es una cadena vacía.
  2. Cada elemento almacenado, uno por línea.

Por ejemplo, un historial podría mostrarse así:

Historial
Ficha blanca: c3 -> d4
Ficha negra: b6 -> c5
Ficha blanca: e3 -> f4

7.6 Clase Tablero

La clase Tablero representa el tablero de juego.

Será la clase principal de la práctica.

Atributos privados

AtributoTipoDescripción
casillasPieza* [8][8]Matriz de punteros a piezas.
turnoColorColor del jugador que tiene el turno.
historialContenedor<std::string, 50>Historial de movimientos realizados.

Funciones

FunciónParámetrosDevuelveDescripción
TableroNingunoNadaConstructor. Inicializa el tablero, el turno y el historial.
~TableroNingunoNadaDestructor. Libera la memoria de las piezas que queden en el tablero.
inicializarNingunovoidColoca las fichas iniciales en el tablero.
esta_dentroint fila, int columnaboolIndica si una posición está dentro del tablero.
esta_vaciaint fila, int columnaboolIndica si una posición está dentro del tablero y no contiene ninguna pieza.
get_piezaint fila, int columnaPieza*Devuelve la pieza situada en una posición, o nullptr si no hay pieza.
moverint fila_origen, int columna_origen, int fila_destino, int columna_destinoboolIntenta mover una pieza desde una posición de origen hasta una posición de destino.
get_turnoNingunoColorDevuelve el color del turno actual.
cambiar_turnoNingunovoidCambia el turno de blanco a negro o de negro a blanco.
get_historialNingunoconst Contenedor<std::string, 50>&Devuelve el historial de movimientos.

Los métodos que no modifican el estado del tablero deberán declararse como constantes.

Constructor

El constructor de Tablero deberá:

  1. Inicializar todas las casillas a nullptr.
  2. Inicializar el turno a Color::BLANCO.
  3. Inicializar el historial con el nombre "Historial".
  4. Llamar al método inicializar.

Destructor

El destructor de Tablero deberá liberar la memoria de todas las piezas que sigan colocadas en el tablero.

Método inicializar

El método inicializar colocará las fichas iniciales en el tablero.

Se colocarán fichas negras en las filas internas 0, 1 y 2.

Se colocarán fichas blancas en las filas internas 5, 6 y 7.

Solo se colocarán fichas en las casillas donde se cumpla:

(fila + columna) % 2 == 1

Método esta_dentro

El método esta_dentro devolverá true si la fila y la columna están dentro del tablero.

Es decir, si ambas están entre 0 y 7.

Método esta_vacia

El método esta_vacia devolverá true si la posición indicada está dentro del tablero y no contiene ninguna pieza.

Método get_pieza

El método get_pieza devolverá el puntero a la pieza situada en la posición indicada.

Si la posición está fuera del tablero o no hay ninguna pieza, devolverá nullptr.

Método mover

El método mover pertenece a la clase Tablero.

Este método intentará mover una pieza desde la posición indicada por fila_origen, columna_origen hasta la posición indicada por fila_destino, columna_destino.

El método mover deberá realizar las siguientes comprobaciones:

  1. La posición de origen debe estar dentro del tablero.
  2. La posición de destino debe estar dentro del tablero.
  3. En la posición de origen debe haber una pieza del jugador que tiene el turno.
  4. La posición de destino debe estar vacía.
  5. El movimiento debe ser válido para el tipo concreto de pieza.
  6. La pieza debe moverse a la posición de destino.
  7. Debe actualizarse la posición interna de la pieza.
  8. Debe añadirse el movimiento al historial.
  9. Si procede, la ficha debe promocionar a dama.
  10. Debe cambiarse el turno.

La comprobación del punto 5 se realizará mediante enlace dinámico llamando al método es_movimiento_valido de la pieza.

Si el movimiento se realiza correctamente, devolverá true.

Si el movimiento no es válido, no modificará el tablero ni el historial y devolverá false.

Promoción dentro de mover

Después de realizar un movimiento válido, el tablero deberá comprobar si la pieza movida debe promocionar a dama.

Una ficha normal blanca promociona si llega a la fila interna 0.

Una ficha normal negra promociona si llega a la fila interna 7.

En caso de promoción:

  1. Se deberá crear una nueva FichaDama del mismo color.
  2. Se deberá sustituir la ficha normal por la nueva dama en la casilla.
  3. La posición interna de la nueva dama deberá coincidir con la casilla de destino.

Tras el movimiento y la promoción, el jugador no repetirá turno, sino que el turno pasa al otro jugador.

Historial de movimientos

Cada movimiento correcto deberá añadirse al historial como una cadena de texto.

Ejemplos:

Ficha blanca: c3 -> d4
Ficha negra: b6 -> c5
Ficha blanca: e3 -> f4

El historial tendrá un máximo de 50 movimientos.

Si el historial está lleno, el movimiento podrá realizarse igualmente, pero no será necesario guardar más movimientos.

Operador de salida

También se deberá implementar el operador de salida para Tablero.

El operador de salida será una función amiga de la clase Tablero.

FunciónParámetrosDevuelveDescripción
operator<<std::ostream& os, const Tablero& tablerostd::ostream&Función amiga que añade al flujo de salida la representación del tablero.

La declaración dentro de la clase será:

friend std::ostream& operator<<(std::ostream& os, const Tablero& tablero);

El tablero se mostrará con las columnas a hasta h y las filas 8 hasta 1. También debe mostrar el color del jugador que tiene el turno actual.

Ejemplo:

Turno: blanco
a b c d e f g h
8 [ ][x][ ][x][ ][x][ ][x] 8
7 [x][ ][x][ ][x][ ][x][ ] 7
6 [ ][x][ ][x][ ][x][ ][x] 6
5 [ ][ ][ ][ ][ ][ ][ ][ ] 5
4 [ ][ ][ ][ ][ ][ ][ ][ ] 4
3 [o][ ][o][ ][o][ ][o][ ] 3
2 [ ][o][ ][o][ ][o][ ][o] 2
1 [o][ ][o][ ][o][ ][o][ ] 1
a b c d e f g h

8 Salida esperada con el mainp4.cc proporcionado

Para probarlo, primero compila con:

Ventana de terminal
make

Después ejecuta:

Ventana de terminal
make run

El ejecutable se encuentra en la carpeta bin.

La salida esperada para el archivo mainp4.cc será la siguiente:

======== ESTADO INICIAL ========
Turno: blanco
a b c d e f g h
8 [ ][x][ ][x][ ][x][ ][x] 8
7 [x][ ][x][ ][x][ ][x][ ] 7
6 [ ][x][ ][x][ ][x][ ][x] 6
5 [ ][ ][ ][ ][ ][ ][ ][ ] 5
4 [ ][ ][ ][ ][ ][ ][ ][ ] 4
3 [o][ ][o][ ][o][ ][o][ ] 3
2 [ ][o][ ][o][ ][o][ ][o] 2
1 [o][ ][o][ ][o][ ][o][ ] 1
a b c d e f g h
======== MOVIMIENTOS ========
======== ESTADO FINAL ========
Turno: negro
a b c d e f g h
8 [ ][x][ ][x][ ][x][ ][x] 8
7 [x][ ][x][ ][x][ ][x][ ] 7
6 [ ][ ][ ][x][ ][x][ ][x] 6
5 [ ][ ][x][ ][ ][ ][ ][ ] 5
4 [ ][ ][ ][o][ ][o][ ][ ] 4
3 [o][ ][ ][ ][ ][ ][o][ ] 3
2 [ ][o][ ][o][ ][o][ ][o] 2
1 [o][ ][o][ ][o][ ][o][ ] 1
a b c d e f g h
======== HISTORIAL ========
Historial
Ficha blanca: c3 -> d4
Ficha negra: b6 -> c5
Ficha blanca: e3 -> f4

9 Uso de make

El fichero Makefile incluye la configuración de la herramienta make con varias opciones que pueden resultarte útiles.

A continuación tienes una lista de las cosas que puedes hacer:

ComandoDescripción
makeCompila y enlaza el código generando el ejecutable p4 dentro de la carpeta bin.
make runCompila, enlaza y ejecuta la práctica.
make runvCompila, enlaza y ejecuta la práctica con Valgrind.
make vstatsMuestra un resumen de pérdidas de memoria detectadas por Valgrind.
make debugCompila y abre el ejecutable con gdb en modo texto.
make tgzBorra archivos compilados y genera el archivo comprimido irp2-p4.tgz.
make cleanElimina los archivos compilados, como ejecutables y objetos.
make checkCompila y ejecuta un corrector básico de interfaz usando check.cc.

El corrector check.cc comprueba que las clases, métodos, parámetros y tipos de retorno principales existen y enlazan correctamente. No comprueba que la lógica del programa sea correcta.

Si has compilado tu programa y vuelves a compilar sin modificar nada del código, make detectará que no necesita compilar nada. Si aun así necesitas recompilar, puedes usar primero:

Ventana de terminal
make clean

y después:

Ventana de terminal
make

Para la ejecución con Valgrind necesitas tener instalado el programa valgrind.

Puedes instalarlo en Linux así:

Ventana de terminal
sudo apt-get install valgrind

Valgrind te será muy útil para detectar:

  • accesos a memoria incorrectos,
  • uso de punteros no válidos,
  • fugas de memoria al no liberar correctamente las piezas creadas dinámicamente.

11 Archivos

A continuación se listan los archivos de la práctica:

  • mainp4.cc: contiene algunas pruebas básicas del programa. Puedes modificarlo para hacer tus propias pruebas, aunque el corrector podrá usar otro archivo principal.
  • src/color.h: contiene el enumerado Color.
  • src/pieza.h: cabecera de la clase Pieza.
  • src/pieza.cc: implementación de la clase Pieza.
  • src/fichanormal.h: cabecera de la clase FichaNormal.
  • src/fichanormal.cc: implementación de la clase FichaNormal.
  • src/fichadama.h: cabecera de la clase FichaDama.
  • src/fichadama.cc: implementación de la clase FichaDama.
  • src/contenedor.h: cabecera de la plantilla Contenedor.
  • src/contenedor.tcc: implementación de la plantilla Contenedor. Aunque tiene extensión .tcc, también debe incluir una guarda de inclusión.
  • src/tablero.h: cabecera de la clase Tablero.
  • src/tablero.cc: implementación de la clase Tablero.
  • Makefile: se recomienda su uso para compilar de forma sencilla.
  • check.cc: archivo de comprobación básica de interfaz. Sirve para probar con make check que las clases, funciones, parámetros y tipos de retorno principales están definidos correctamente. No debes modificarlo.

Debes implementar los siguientes archivos, que se encuentran en la carpeta src:

color.h
pieza.h
pieza.cc
fichanormal.h
fichanormal.cc
fichadama.h
fichadama.cc
contenedor.h
contenedor.tcc
tablero.h
tablero.cc

El archivo mainp4.cc se proporciona como ejemplo de prueba.

Puedes modificarlo durante el desarrollo para hacer tus propias pruebas, pero no debes depender de él para que la práctica sea correcta. El servidor de prácticas usará sus propios archivos de prueba.

Para probar la promoción durante el desarrollo, puedes crear temporalmente un tablero con una única ficha normal cerca del extremo opuesto. Esto sería fácil de probar si modificas temporalmente la función inicializar de Tablero, añadiendo por ejemplo una única ficha de cada color. Si haces este cambio, recuerda volver a modificarlo cuando vayas a entregar la práctica con la implementación que debes realizar.


11 Entrega. Requisitos técnicos

Requisitos que tiene que cumplir este trabajo práctico para considerarse válido y ser evaluado. Si no se cumple alguno de los requisitos, la calificación será cero.

  • El archivo entregado se llama irp2-p4.tgz, todo en minúsculas.
  • Dentro de dicho archivo habrá una carpeta llamada irp2-p4.
  • Dentro de la carpeta irp2-p4 deberá haber una carpeta llamada src.
  • Dentro de la carpeta src deberán estar, como mínimo, todos los archivos que debes implementar.
  • No es necesario entregar Makefile, mainp4.cc ni check.cc. Si se hace, no pasa nada.
  • El servidor de prácticas usará su propio Makefile y sus propios archivos de prueba.
  • Todos los archivos .h deben tener guardas de inclusión con #ifndef, #define y #endif.
  • El archivo contenedor.tcc también debe tener guarda de inclusión.
  • El archivo contenedor.h debe incluir contenedor.tcc al final, para que la implementación de la plantilla esté disponible al compilar.
  • El archivo contenedor.tcc no debe compilarse por separado; será incluido desde contenedor.h. Considéralo como un archivo .h, no como un .cc.
  • La clase Contenedor no debe usar std::vector.
  • Los operadores de salida operator<< deberán implementarse como funciones amigas (friend).

Se recomienda ejecutar make check antes de entregar para comprobar que la interfaz pública de la práctica compila correctamente. Sin embargo, que make check funcione no significa que la práctica esté correctamente resuelta. Solo indica que la interfaz pública básica coincide con la esperada y que no faltan implementaciones necesarias para enlazar el programa y compilar.

El archivo comprimido lo puedes crear así en el terminal, situándote en la carpeta padre de irp2-p4:

Ventana de terminal
tar cfz irp2-p4.tgz irp2-p4

También puedes emplear el archivo Makefile que tienes en la carpeta de la práctica para generar el fichero de la entrega:

Ventana de terminal
make tgz

Al descomprimir el archivo irp2-p4.tgz se debe crear un directorio de nombre irp2-p4, todo en minúsculas.

Dentro del directorio irp2-p4 estará la carpeta src, y dentro de ella estarán al menos los archivos necesarios para corregir la práctica.

Las clases y métodos implementados se llaman como se indica en el enunciado, respetando en todo caso el uso de mayúsculas y minúsculas. También es imprescindible respetar estrictamente los textos y los formatos de salida que se indican en este enunciado.

Al principio de todos los ficheros fuente (.h, .cc y .tcc) entregados y escritos por ti se debe incluir un comentario con el nombre y el NIF, DNI o equivalente de la persona que entrega la práctica, como en el siguiente ejemplo:

// NIF: 12345678-Z
// NOMBRE: GARCIA PEREZ, LAURA

Un error de compilación o de enlace implicará un cero en la práctica.

Se utilizará Valgrind para comprobar que no haya fugas de memoria en tu programa. Puedes instalarlo y probarlo antes de realizar la entrega para asegurarte de corregir posibles errores relacionados con la gestión de memoria dinámica.

En este caso, la estructura mínima del fichero que debes entregar es la siguiente:

irp2-p4.tgz
└── irp2-p4
└── src
├── color.h
├── pieza.h
├── pieza.cc
├── fichanormal.h
├── fichanormal.cc
├── fichadama.h
├── fichadama.cc
├── contenedor.h
├── contenedor.tcc
├── tablero.h
└── tablero.cc

Si incluyes también Makefile, mainp4.cc o check.cc, no pasa nada, pero no son necesarios para la entrega. El corrector del servidor utilizará sus propios archivos.

Lugar y fecha de entrega: recuerda que las entregas se realizan siempre en, y solo en, https://pracdlsi.dlsi.ua.es en las fechas y condiciones allí publicadas. Puedes entregar la práctica tantas veces como quieras; solo se corregirá la última entrega. El usuario y contraseña para entregar prácticas es el mismo que se utiliza en UACloud.


13 Recomendaciones

Se recomienda avanzar en este orden:

  1. Implementar Color.
  2. Implementar Pieza.
  3. Implementar FichaNormal.
  4. Implementar FichaDama.
  5. Implementar Contenedor<T, CAPACIDAD>.
  6. Implementar Tablero.
  7. Probar la inicialización del tablero.
  8. Probar movimientos simples de FichaNormal.
  9. Probar la promoción a dama.
  10. Probar movimientos simples de FichaDama.
  11. Probar el historial.

Es recomendable compilar con frecuencia. No esperes a tener toda la práctica terminada para ejecutar make.

También es recomendable utilizar nombres de parámetros descriptivos. Por ejemplo, se preferirá fila_origen frente a abreviaturas como fo, para que el código sea más legible.


14 Detección de plagios/copias

En el Grado en Ingeniería Robótica, la Programación es una materia muy importante. La manera más efectiva de aprender a programar es programando y, por tanto, haciendo las prácticas correspondientes además de otros programas.

Tratar de aprobar las asignaturas de programación sin programar, copiando, es complicado y, además, te planteará problemas para seguir otras asignaturas y también en tu futura vida laboral.

En una asignatura como Programación-II es difícil que alguien que no ha hecho las prácticas supere los exámenes de teoría o el de recuperación de prácticas.

IMPORTANTE:

  • Cada práctica debe ser un trabajo original de la persona que la entrega.
  • En caso de detectarse indicios de copia de una o más prácticas, se tomarán las medidas disciplinarias correspondientes, informando a las direcciones de Departamento y de la EPS por si hubiera lugar a otras medidas disciplinarias adicionales.