36. Frontón (5): Descomposición en clases

Descomponer un programa en clases es una tarea que no suele ser sencilla, especialmente si el programa del que partimos es grande. En nuestro caso es sólo un fuente de 120 líneas, yy aun así va a ser trabajoso...

El primer paso es descomponer el problema, pensando qué "objetos" lo forman y cómo interaccionan entre ellos. Por ejemplo, en nuestro caso tenemos una "Pelota", una "Raqueta", una "Pared de ladrillos", un "Marcador". Pero esta descomposición no tiene por qué ser única: también se podría pensar que cada uno de los ladrillos fuera también un tipo de objeto independiente (una "clase"), o crear una "Presentación". El detallar más o menos depende de cada caso, de la complejidad de lo que queda por descomponer, incluso de los criterios de cada diseñador. Por supuesto, suele haber una clase que representa a toda la aplicación (en nuestro caso, al juego, "Frontón").

Una vez hemos decidido qué clases van a formar nuestro programa, hay que analizar qué tareas tendrá que realizar cada una de ellos (los "métodos"). Este paso también puede ayudar a descubrir clases que hubiéramos pasado por alto. Por ejemplo, en nuestro caso, la raqueta se podría "Mover a la derecha" y "Mover a la izquierda", mientras que la pelota simplemente tendría un "Mover", que la desplazase a la siguiente posición que le correspondiera. La "Pared de ladrillos podría tener un método que permitiera saber si hay un ladrillo en una cierta posición, o que permitiera romper un cierto ladrillo. Todas ellas tendrían también un método "Mostrar" o "Dibujar", para representarlas en pantalla en su posición actual y con su estado actual. También podrían tener un "Reiniciar" que llevara cada elemento a su posición inicial y con su estado inicial (la velocidad prevista, para la pelota, o todos los ladrillos de partida, para la pared).

Fronton, clases

El siguiente paso, que también nos puede ayudar a descubrir clases que hemos pasado por alto, es plantearnos cómo podría ser el "Main" del programa y de qué forma usaría cada una de esas clases, buscando que el programa resultante sea tan legible como nos sea posible. Por ejemplo, un fragmento podría ser así:

// Dibujo los ladrillos y la pelota
Console.Clear();
miPared.Mostrar();
miRaqueta.Mostrar();            
miPelota.Mostrar();
miMarcador.Mostrar();
 
// Compruebo colisiones con el fondo
if (miPared.HayLadrillo(miPelota.GetX(), miPelota.GetY()))
{
    miPelota.RebotarVertical();
    miPared.Romper(miPelota.GetX(), miPelota.GetY());
    miMarcador.AumentarPuntos(10);
}

El fuente completo, incuyendo todas las clases en un único fichero (que es una práctica poco recomendable, pero que usaremos en este primer acercamiento) podría ser:

// Fronton - version 6: descompuesto en clases
 
using System;
using System.Threading;
 
class FrontonClases
{
    static Raqueta miRaqueta;
    static Ladrillos miPared;
    static Pelota miPelota;
    static Marcador miMarcador;
 
    public static void Main()
    {
        miRaqueta = new Raqueta();
        miPared = new Ladrillos();
        miPelota = new Pelota();
        miMarcador = new Marcador();
 
        int minX = 0, maxX = 79; // Límites horizontales
        int maxY = 23;           // Límites verticales
        bool terminado = false;
        ConsoleKeyInfo tecla;
 
        do
        {
            // Dibujo los ladrillos y la pelota
            Console.Clear();
            miPared.Mostrar();
            miRaqueta.Mostrar();            
            miPelota.Mostrar();
            miMarcador.Mostrar();
 
            // Muevo la raqueta si se pulsa alguna tecla
            if (Console.KeyAvailable)
            {
                tecla = Console.ReadKey(false);
                if (tecla.Key == ConsoleKey.RightArrow 
                        && (miRaqueta.GetX() + miRaqueta.GetLongitud() - 1 < maxX))
                    miRaqueta.MoverDerecha();
                if (tecla.Key == ConsoleKey.LeftArrow 
                        && miRaqueta.GetX() > 1)
                    miRaqueta.MoverIzquierda();
            }
 
            // Compruebo colisiones con el fondo
            if (miPared.HayLadrillo(miPelota.GetX(), miPelota.GetY()))
            {
                miPelota.RebotarVertical();
                miPared.Romper(miPelota.GetX(), miPelota.GetY());
                miMarcador.AumentarPuntos(10);
            }
 
            // Y colisiones con la raqueta
            if ((miPelota.GetX() >= miRaqueta.GetX() 
                && miPelota.GetX() <= miRaqueta.GetX() + miRaqueta.GetLongitud() - 1)
                && miPelota.GetY() == miRaqueta.GetY())
                miPelota.RebotarVertical();
 
            // Compruebo si llego al límite de la pantalla
            // Si se sale por abajo...
            if (miPelota.GetY() >= maxY)
            {
                miPelota.Reiniciar();
                miRaqueta.Reiniciar();
                miMarcador.RestarVida();
                if (miMarcador.GetVidas() == 0)
                {
                     // Reinicio datos
                     miMarcador.Reiniciar();
                     miPared.Reiniciar();
                     // Pantalla de presentación
                     Console.Clear();
                     Console.WriteLine("Pulsa intro para jugar...");
                     Console.ReadLine();
                }
            }
            // // Si llega a un extremo sin salir por abajo: rebota
            if ((miPelota.GetX() <= minX) 
                    || (miPelota.GetX() >= miPared.GetAnchura()-1))
                miPelota.RebotarHorizontal(); 
            if (miPelota.GetY() >= maxY)
                miPelota.RebotarVertical();
 
            // Y paso a la siguiente posición
            miPelota.Mover();
 
            // Y espero un poco hasta el siguiente fotograma
            Thread.Sleep(40);
 
        } while (! terminado);
    }
}
 
// ----------------------------------------------------------------
 
class Ladrillos
{
    private string[] filasLadrillos = 
    { "-------=-------=-------=-------=-------=-------=-------=-------=---",
      "------=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=----",
      "-----=---=---=---=---=---=---=---=---=---=---=---=---=---=---=-----",
      "----=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=------",
      "---=-------=-------=-------=-------=-------=-------=-------=-------"
    };
 
    public void Reiniciar()
    {
        filasLadrillos[0] = "-------=-------=-------=-------=-------=-------=-------=-------=---";
        filasLadrillos[1] = "------=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=----";
        filasLadrillos[2] = "-----=---=---=---=---=---=---=---=---=---=---=---=---=---=---=-----";
        filasLadrillos[3] = "----=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=-----=-=------";
        filasLadrillos[4] = "---=-------=-------=-------=-------=-------=-------=-------=-------";
    }
 
    public void Mostrar()
    {
        Console.SetCursorPosition(0, 0);
        foreach (string filaActual in filasLadrillos)
            Console.WriteLine(filaActual);
    }
 
    public int GetAltura()
    {
        return filasLadrillos.Length;
    }
 
    public int GetAnchura()
    {
        return filasLadrillos[0].Length;
    }
 
    public char GetContenido(int x, int y)
    {
        return filasLadrillos[y][x];
    }
 
    public bool HayLadrillo(int x, int y)
    {
        if ( y >= GetAltura() ) return false;
        if ( x >= GetAnchura() ) return false;
        if (filasLadrillos[y][x] == ' ') return false;
        return true;
    }
 
    public void Romper(int x, int y)
    {
        filasLadrillos[y] = filasLadrillos[y].Remove(x, 1);
        filasLadrillos[y] = filasLadrillos[y].Insert(x, " ");
    }
}
 
// ----------------------------------------------------------------
 
class Pelota
{
    private int x, y;
    private char simbolo;
    int incrX, incrY;
 
    public Pelota()
    {
        simbolo = 'O';
        Reiniciar();
    }
 
    public void Reiniciar()
    {
        x = 40;
        y = 12;
        incrX = 1;
        incrY = -1;
    }
 
    public void Mostrar()
    {
        Console.SetCursorPosition(x,y);
        Console.Write(simbolo);
    }
 
    public void Mover()
    {
        x += incrX;
        y += incrY;
        // Posible fallo al rebotar en los extremos
        if (x<0) x=0;
        if (y<0) y=0;
    }
 
    public void RebotarHorizontal()
    {
        incrX = -incrX;
    }
 
    public void RebotarVertical()
    {
        incrY = -incrY;
    }
 
    public int GetX()
    {
        return x;
    }
 
    public int GetY()
    {
        return y;
    }
}
 
// ----------------------------------------------------------------
 
class Raqueta
{
    private int x;
    private int y;
    private string dibujo;
 
    public Raqueta()
    {
        dibujo = "__________";
        Reiniciar();
    }
 
    public void Reiniciar()
    {
        x = 39;
        y = 22;
    }
 
    public void Mostrar()
    {
        Console.SetCursorPosition(x, y);
        Console.Write(dibujo);
    }
 
    public void MoverDerecha()
    {
        x += 2;
    }
 
    public void MoverIzquierda()
    {
        x -= 2;
    }
 
    public int GetX()
    {
        return x;
    }
 
    public int GetY()
    {
        return y;
    }
 
    public int GetLongitud()
    {
        return dibujo.Length;
    }
}
 
// ----------------------------------------------------------------
 
class Marcador
{
    private int x, y;
    private int puntos, vidas;
 
    public Marcador()
    {
        x = 0;
        y = 23;
        Reiniciar();
    }
 
    public void Reiniciar()
    {
        puntos = 0;
        vidas = 3;
    }
 
    public void Mostrar()
    {
        Console.SetCursorPosition(x, y);
        Console.Write("Vidas: {0}    Puntos: {1}", vidas, puntos);
    }
 
    public int GetVidas()
    {
        return vidas;
    }
 
    public void AumentarPuntos(int cantidad)
    {
        puntos += cantidad; 
    }
 
    public void RestarVida()
    {
        vidas--;
    }
}
 

Ejercicio propuesto: Mejora este fuente para que cada elemento tenga su propio color (distinto para la pelota y para la raqueta o cada tipo de ladrillo).