TLlop - Clon del Tetris

Llop Site Home > JAVA > TLlop > Ventana de juego

Ventana de juego:

Clase Tetris

package ejercicio_5;

import java.awt.*;
import java.io.*;
import java.net.*;
import javax.sound.sampled.*;
import javax.swing.*;

/** Título: TLlop
    Descripción: Prácticas capítulo 11, ejercicio 5
    Copyright: Copyright (c) 2005
    Empresa: Casa_Llop
    @author Albert_Lobo
    @version 1.5  */

/** Clase principal del programa. */
public class Tetris extends JApplet
{
  public final static short ANCHO = 550;
  public final static short ALTO = 480;
  private PanelJuego juego;

  /** Ejecuta el programa como applet. Los componentes swing deberían ser creados, consultados,
      y manipulados en el hilo que trata los eventos, pero los navegadores de internet
      no pueden invocar los métodos principales de un applet desde ese hilo, así que para ejecutar
      código de swing en el hilo que trata eventos hay que usar 'SwingUtilities.invokeAndWait()'. */
  public void init ()
  {
    try
    {
      SwingUtilities.invokeAndWait (new Runnable ()
      {
        public void run ()
        {
          /** Añade la ventana de juego al applet, pone a éste un tamaño fijo, y saca
              el diálogo inicial. */
          juego = new PanelJuego ();
          getContentPane ().add (juego);
          setSize (new Dimension (ANCHO, ALTO));
          juego.setSize (new Dimension (ANCHO, ALTO));
          juego.muestraDialogoMenuInicio ();
          /** Ahora que el panel de juego tiene sus dimensiones inicializadas,
              ya se le puede poner fondo. */
          juego.setFondo (juego.getFondo ());
        }
      });
    } catch (Exception excepcion) {
      System.err.println ("Error al intentar arrancar el juego.");
    }
  }

  /** Iniciar el applet. En la práctica, es como el botón 'Pausar/Despausar'. */
  public void start ()
  {
    juego.requestFocus ();
    if ((juego.getPausa ()) && (juego.hayPieza ()))
      juego.pausarDespausar ();
  }

  /** Detener el applet. En la práctica, es como el botón 'Pausar/Despausar'. */
  public void stop ()
  {
    if (!juego.getPausa ())
      juego.pausarDespausar ();
  }

  /** Obtener información del applet. */
  public String getAppletInfo ()
  {
    if (!juego.getPausa ())
      juego.pausarDespausar ();
    return "Información del applet";
  }

  /** Obtener información del parámetro. */
  public String [][] getParameterInfo ()
  {
    if (!juego.getPausa ())
      juego.pausarDespausar ();
    return null;
  }

  /** Método estático que devuelve la imagen en el fichero de recursos cuyo nombre corresponde con el
      del primero de los argumentos. El segundo argumento es una descripción del icono para
      su constructor, que, mediante tecnología de ayuda, permite a invidentes saber qué representa.
      @param imagen String       : Nombre del archivo de la imagen.
      @param descripcion String  : Descripción para el icono que se devolverá.
      @return ImageIcon          : Icono con la imagen apropiada del fichero de recursos. */
  public static ImageIcon getImagen (String imagen, String descripcion)
  {
    URL uRL = Tetris.class.getResource ("rscTLlop/" + imagen) ;
    return new ImageIcon (uRL, descripcion);
  }

  /** Método que devuelve un flujo de entrada para datos de audio desde el clip cuyo nombre
      se especifica en el argumento.
      @param clip String        : Nombre del clip.
      @return AudioInputStream  : Flujo de datos de retorno. */
  public static AudioInputStream getClip (String clip)
  {
    AudioInputStream retorno = null;
    URL uRL = Tetris.class.getResource ("rscTLlop/" + clip) ;
    try
    {
      retorno = AudioSystem.getAudioInputStream (uRL);
    } catch (IOException ex) {
      System.err.println (clip + " no encontrado.");
    } catch (UnsupportedAudioFileException ex) {
      System.err.println (clip + " no puede sonar.");
    }
    return retorno;
  }
}




Clase PanelJuego

import java.lang.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

/** Clase que extiende 'JPanel', y que reúne la cuadrícula de juego y el panel con info de la partida
    -también llamado 'heads up display'. */
public class PanelJuego extends JPanel implements KeyListener
{
  /** Cadena constante con el nobre del archivo que contiene el fondo de ventana 1. */
  public final static String FONDO1 = "fondo1.jpg";
  /** Cadena constante con el nobre del archivo que contiene el fondo de ventana 2. */
  public final static String FONDO2 = "fondo2.jpg";
  /** Cadena constante con el nobre del archivo que contiene el fondo de ventana 3. */
  public final static String FONDO3 = "fondo3.jpg";
  /** Cadena constante con el nobre del archivo que contiene el fondo de ventana 4. */
  public final static String FONDO4 = "fondo4.jpg";

  // Márgen del panel.
  private Insets margen;
  // Objeto para las opciones del juego.
  private OpcionesJuego opcionesJuego;
  // El panel/cuadrícula de juego.
  private PanelCuadricula cuadricula;
  // El panel con los datos de la partida.
  private HUD headsUpDisplay;
  // La cuadrícula para mostrar la siguiente pieza.
  private PanelCuadricula cuadriculaSiguiente;
  // Tabla con la ID de las piezas disponibles en una fila, y en la otra su frecuencia de aparición.
  private float piezas [][];
  // El hilo que controlará la dinámica de las partidas.
  private HiloJuego hiloJuego;

  // Variable para el número de líneas completadas en la partida.
  private short lineasCompletas;
  // Variable para el nivel de dificultad de la partida.
  private byte nivel;
  // Variable que contabiliza cuántos niveles ha subido el usuario pulsando la tecla de 'Subir Nivel'.
  private byte offsetNivel;
  // Variable que determina cuántas líneas deben completarse para subir de nivel en la partida.
  private byte lineasCambioNivel;
  // Variable para la puntuación en la partida.
  private long puntuacion;
  // Variable que cuenta las reacciones en cadena que se suceden en un mismo 'reventón' de líneas.
  private byte numeroReacciones;

  // Instancia de la pieza para la cuadrícula de juego.
  private Pieza piezaActual;
  // Siguiente pieza que saldrá a la cuadrícula de juego.
  private Pieza siguientePieza;
  // Descripción de los cuadradillos que forman las piezas.
  private String disenoPieza;
  // Variable para la imagen de fondo.
  private Image imagenFondo;
  // Variable con el nombre del archivo que contiene la imagen de fondo de la ventana.
  private String fondo;
  // Objeto que lleva el seguimiento del estado de la imagen de fondo; conviene porque ésta debe ser
  // escalada para llenar la ventana, y el método 'getScaledInstance ()', de 'Image', a veces tarda
  // un rato -'MediaTracker' tiene recursos para pausar la ejecución del programa hasta que la imagen
  // haya sido escalada.
  private MediaTracker seguidorEstado;
  // Objeto para las músicas y los efectos sonoros.
  private Sonido sonido;
  // 'Alarma' para programar algunos sucesos.
  private java.util.Timer alarma;
  // Variable que indica si la siguiente pieza está visible o no.
  private boolean seVeSiguientePieza;
  // Tabla para los 'virtual key codes' de las teclas de los controles.
  private int controles [];

  // 'JOptionPane' para el 'PanelMenuInicio'.
  private JOptionPane optionPane;
  // Este panel será pará el diálogo de inicio.
  private PanelMenuInicio panelMenuInicio;

  /** Constructor. Lo único que hace es inicializar las variables de la calse. */
  public PanelJuego ()
  {
    super ();

    /** Pone un márgen de 16 píxels al panel. */
    margen = new Insets (16, 16, 16, 16);
    setBorder (new EmptyBorder (margen));

    /** Lo primero es inicializar 'opcionesJuego', ya que muchas de sus variables servirán para
        inicializar las de esta clase (PanelJuego). Se busca el archivo de opciones en el disco
        por si no es la primera vez que se juega. */
    opcionesJuego = new OpcionesJuego ();

    /** Se inicializan las variables relacionadas con la imagen de fondo. */
    imagenFondo = Tetris.getImagen (opcionesJuego.getFondoPanel (), "Fondo de la ventana").getImage ();
    fondo = opcionesJuego.getFondoPanel ();
    seguidorEstado = new MediaTracker (this);
    /** Se inicializa la alarma. */
    alarma = new java.util.Timer ();
    /** Se inicializan ambas cuadrículas y se les pone el fondo. */
    cuadricula = new PanelCuadricula (opcionesJuego.getCuadradosX (), opcionesJuego.getCuadradosY (),
                                     (byte) 3, opcionesJuego.getDiseno (),
                                     opcionesJuego.getFondoCuadricula (), opcionesJuego.getGravedadReal ());
    cuadricula.setFondo (opcionesJuego.getFondoCuadricula ());
    // La cuadrícula para la siguiente pieza es de 6 x 6 cuadradillos, y se dibuja a la mitad
    // de su tamaño real. Se le pone un tamaño fijo.
    cuadriculaSiguiente = new PanelCuadricula ((byte) 6, (byte) 6, (byte) 0, opcionesJuego.getDiseno (),
                                              opcionesJuego.getFondoCuadricula (), false);
    cuadriculaSiguiente.setEscala ((float) .8);
    cuadriculaSiguiente.setPreferredSize (new Dimension (112, 112));
    cuadriculaSiguiente.setFondo (opcionesJuego.getFondoCuadricula ());
    /** Se inicializa el 'hedsUpDisplay' y el hilo de juego. */
    headsUpDisplay = new HUD ();
    hiloJuego = new HiloJuego ();

    /** El resto de variables se inicializan mediante esta función, que ajusta las variables
        necesarias para una nueva partida. El número de reacciones en cadena se inicializa a 1. */
    actualizaOpcionesNuevoJuego ();
    numeroReacciones = 1;

    /** Se añade al panel la cuadrícula de juego y el 'HUD'. */
    add (cuadricula, BorderLayout.CENTER);
    add (headsUpDisplay, BorderLayout.EAST);
    /** Se añade al panel el oyente para el teclado. */
    addKeyListener (this);

    /** Finalmente, se inicializa la clase con el sonido, y el panel para el diálogo de inicio. */
    sonido = new Sonido ();
    panelMenuInicio = new PanelMenuInicio (opcionesJuego);
  }

  /** Método que muestra el diálogo de inicio para que el usuario pueda hacer los cambios que quiera
      en las diferentes opciones del juego. Al cerrarse, los cambios quedan guardados. */
  public void muestraDialogoMenuInicio ()
  {
    /** Se deactivan los botones del 'HUD' para que el usuario no pueda trastear con ellos. */
    headsUpDisplay.activaDesactivaBotones (false);
    /** Se refrescan los sub-paneles del panel 'OPCIONES', por si se ha hecho algún cambio
        durante la partida. */
    panelMenuInicio.ajustaEtiquetasVariablesOpciones ();
    String [] options = {"Empezar"};
    int retorno = optionPane.showOptionDialog (null, panelMenuInicio, "Menú de Inicio",
                                               JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE,
                                               null, options, options[0]);
    if (retorno == JOptionPane.OK_OPTION)
      actualizaOpciones();
    /** Se define el mensaje del primero botón del 'HUD', y todos se activan de nuvo. */
    headsUpDisplay.setMensajeBotonPausaReanuda ("Jugar");
    headsUpDisplay.activaDesactivaBotones (true);
    /** El panel necesita recuperar el foco para capturar 'KeyEvents'. */
    requestFocus ();
  }

  /** Reajusta las variables cuya modificación puede ser reflejada en el juego durante una partida. */
  private void actualizaOpciones ()
  {
    /** Sólo modifica aquellas variables que han sido reajustadas. */
    if (piezas != opcionesJuego.getPiezasDisponibles ())
      piezas = opcionesJuego.getPiezasDisponibles ();
    if (disenoPieza != opcionesJuego.getDiseno ())
    {
      disenoPieza = opcionesJuego.getDiseno ();
      cuadricula.setDiseno (opcionesJuego.getDiseno ());
    }
    if (fondo != opcionesJuego.getFondoPanel ())
      setFondo (opcionesJuego.getFondoPanel ());
    if (cuadricula.getFondo () != opcionesJuego.getFondoCuadricula ())
    {
      cuadricula.setFondo (opcionesJuego.getFondoCuadricula ());
      cuadriculaSiguiente.setFondo (opcionesJuego.getFondoCuadricula ());
    }
    if (cuadricula.getGravedadReal () != opcionesJuego.getGravedadReal ())
      cuadricula.setGravedadReal (opcionesJuego.getGravedadReal ());
    if (seVeSiguientePieza != opcionesJuego.getSeVeSiguientePieza ())
      seVeSiguientePieza = opcionesJuego.getSeVeSiguientePieza ();
    if (controles != opcionesJuego.getControles ())
      controles = opcionesJuego.getControles ();
  }

  /** Reajusta las variables cuyo cambio sólo puede tener lugar antes de empezar una nueva partida.
      Luego llama a 'actualizaOpciones ()'. */
  private void actualizaOpcionesNuevoJuego ()
  {
    /** Al iniciar la partida, el usuario todavía no ha tenido la oportunidad de 'subir nivel'. */
    offsetNivel = 0;
    /** Como en el anterior método, sólo cambian las variables modificadas. */
    if (lineasCambioNivel != opcionesJuego.getLineasCambioNivel ())
      lineasCambioNivel = opcionesJuego.getLineasCambioNivel ();
    if (nivel != opcionesJuego.getNivelInicial ())
      nivel = opcionesJuego.getNivelInicial ();
    if ((cuadricula.getAnchura () != opcionesJuego.getCuadradosX ()) ||
       (cuadricula.getAltura () != opcionesJuego.getCuadradosY ()))
    {
      cuadricula.setAnchura (opcionesJuego.getCuadradosX ());
      cuadricula.setAltura (opcionesJuego.getCuadradosY ());
      cuadricula.ajustarTamano ();
      cuadricula.setFondo (opcionesJuego.getFondoCuadricula ());
    }
    if (cuadricula.getDiseno () != opcionesJuego.getDiseno ())
      cuadricula.setDiseno (opcionesJuego.getDiseno ());
    actualizaOpciones ();
  }

  /** Método que inicia una partida. */
  public void comienza ()
  {
    /** Efecto sonoro de 'comenzar partida'. */
    sonido.start ();

    /** Las líneas y la puntuación vuelven a 0. */
    lineasCompletas = 0;
    puntuacion = 0;

    /** Se inicializan las dos primeras piezas. */
    siguientePieza = piezaAleatoria ();
    inicializaPiezas ();

    /** Se refresca el 'HUD' con los datos de esta nueva partida. */
    headsUpDisplay.setPuntuacion (puntuacion);
    headsUpDisplay.setNivel (nivel);
    headsUpDisplay.setLineasCompletas (lineasCompletas);

    /** Arranca el hilo que repinta la cuadrícula, y empieza la animación de las 'letras decrecientes'
        -para esto, hay que poner el texto de turno, darle tamaño a la fuente, e indicar que el texto
        se pinte (en 'paint (Graphics)' de 'PanelCuadricula')-; también arranca el hilo que repinta
        la cuadrícula de la siguiente pieza. */
    cuadricula.arrancaHilo ();
    cuadricula.setRefrescoConstante (true);
    cuadricula.setMensaje (PanelCuadricula.JUGAR);
    cuadricula.setTamanoFuenteMensaje ((byte) 60);
    cuadricula.setPintaMensaje (true);
    cuadriculaSiguiente.arrancaHilo ();
    cuadriculaSiguiente.setRefrescoConstante (true);

    /** Se programa la alarma para dentro de un segundo y medio. Entonces, el texto desaparecerá
        de la cuadrícula, y arrancará el hilo de juego. */
    alarma.schedule (new java.util.TimerTask ()
    {
      public void run ()
      {
        cuadricula.setPintaMensaje (false);
        hiloJuego.reset (nivel);
      }
    }, 1500);
  }

  /** Método a ejecutar cuando termina una partida. Principalmente está para activar la
      animación de las 'letras crecientes' antes de seguir con 'completaGameOver ()'. */
  public void gameOver ()
  {
    /** Si el usuario tiene una tecla pulsada al llegar al constructor de 'EntraRecord', el
        panel de éste no saldrá completo -faltan los componentes-; por eso hay que quitar
        el oyente del teclado temporalmente. También hay que desactivar los botones del 'HUD',
        ya que por ahora no deben ser tocados. */
    removeKeyListener (this);
    headsUpDisplay.activaDesactivaBotones (false);

    /** Se pausa el hilo de juego para que no interfiera. */
    hiloJuego.setPausa (true);

    /** Según la puntuación sea récord o no, sonará un efecto sonoro 'feliz' o uno 'triste'. */
    if (puntuacion > opcionesJuego.getUltimoRecord ())
      sonido.hiScore ();
    else
      sonido.gameOver ();

    /** Se activa la animación con el texto 'Game Over' en la cuadrícula de juego, y al cabo de
        un segundo y medio, la alarma ejecuta la función para completar el fin de partida. */
    cuadricula.setMensaje (PanelCuadricula.GAME_OVER);
    cuadricula.setTamanoFuenteMensaje ((byte) 20);
    cuadricula.setPintaMensaje (true);
    alarma.schedule (new java.util.TimerTask ()
    {
      public void run ()
      {
        completaGameOver ();
      }
    }, 1500);
  }

  /** Función con la que se termina la partida. */
  private void completaGameOver ()
  {
    /** Termina la animación del texto, y se anulan las piezas en juego. */
    cuadricula.setRefrescoConstante (false);
    cuadricula.setPintaMensaje (false);
    cuadriculaSiguiente.setRefrescoConstante (false);
    piezaActual = null;
    siguientePieza = null;

    /** Si la puntuación es bastante buena como para entrar en el 'Top 10', salta el diálogo
        que pide el nombre al jugador. */
    if (puntuacion > opcionesJuego.getUltimoRecord ())
      new EntraRecord (puntuacion);

    /** Ahora se muestra el diálogo de inicio con el panel del 'Top 10'. Esto permite al jugador
        hacer cambios para la siguiente partida. */
    panelMenuInicio.seleccionaPestana (PanelMenuInicio.TOP10);
    muestraDialogoMenuInicio ();

    /** Se recupera el oyente del teclado. */
    addKeyListener (this);
  }

  /** Método que ejecuta el hilo de juego, y se encarga de darle la dinámica al juego. Si la
      pieza toca fondo, toma las medidas consecuentes, y si no, la mueve abajo. Es 'synchronized'
      para que no interfiera con los eventos del teclado. */
  public synchronized void movimiento ()
  {
    if (piezaActual.tocaFondo ())
    {
      sonido.pum ();        // Al depositarse, la pieza hace 'pum'.
      piezaTocaFondo ();
    }
    else
      piezaActual.abajo ();
  }

  /** Método que pausa el juego, y muestra el diálogo de inicio en el panel 'Opciones'. */
  private void mostrarOpciones ()
  {
    pausar ();
    panelMenuInicio.seleccionaPestana (PanelMenuInicio.OPCIONES);
    muestraDialogoMenuInicio ();
  }

  /** Método que sube un nivel la velocidad del hilo de juego. */
  private void subeNivel ()
  {
    /** 20 es el nivel máximo: no tiene sentido hacer más rápido el juego -ya lo es bastante. */
    if (nivel > 19)
      return;
    nivel++;                           // Sube el 'nivel'.
    headsUpDisplay.setNivel (nivel);   // Se ajusta la etiqueta en el 'HUD'.
    hiloJuego.ajustaEspera (nivel);    // Se acelera el hilo de juego.
    sonido.subeNivel ();               // Suena el efecto sonoro.
  }

  /** Método que reajusta el poder ver la siguiente pieza en la cuadrícula pequeña o no. */
  private void ajustaSeVeSiguientePieza ()
  {
    seVeSiguientePieza = !seVeSiguientePieza;
    /** El cambio también debe producirse en 'opcionesJuego'. */
    opcionesJuego.setSeVeSiguientePieza (seVeSiguientePieza);
    /** Si la pieza no se veía, hay que pegarla a la cuadrícula, y activar su 'refresco constante'. */
    if (seVeSiguientePieza)
    {
      siguientePieza.pegar (cuadriculaSiguiente, true);
      cuadriculaSiguiente.setRefrescoConstante (true);
    }
    /** Si la pieza se veía, se desactiva el refresco constante, y se limpia la cuadrícula pequeña. */
    else
    {
      cuadriculaSiguiente.setRefrescoConstante (false);
      cuadriculaSiguiente.limpia ();
    }
  }

  /** Método 'synchronized' para no interferir con el oyente del teclado ni el hilo de juego.
      Comienza una partida nueva; si ya ha empezado, pausa o despausa el juego. */
  public synchronized void pausarDespausar ()
  {
    /** Como aquí se puede llegar pulsando un botón del 'HUD', hay que asegurarse que el foco
        vuelve al 'PanelJuego'. */
    requestFocus ();
    /** Si la pieza actual es 'null', el juego está por empezar. Hay que:
          - Poner el mensafe adecuado en el botón del 'HUD'.
          - Vaciar la cuadrícula de juego.
          - E inicializar las variables partinentes; el juego ya está listo para empezar. */
    if (piezaActual == null)
    {
      headsUpDisplay.setMensajeBotonPausaReanuda ("Pausar");
      cuadricula.limpia ();
      actualizaOpcionesNuevoJuego ();
      /** Hay que poner las líneas rellenas iniciales. */
      cuadricula.ponLineasRellenas (opcionesJuego.getLineasRellenasInicio ());
      comienza ();
    }
    /** Si el hilo de juego está pausado, lo despausa; si no lo está, lo pausa.
        El botón del 'HUD' debe reflejar siempre esos cambios. */
    else if (hiloJuego.getPausa ())
    {
      headsUpDisplay.setMensajeBotonPausaReanuda ("Pausar");
      despausar ();
    }
    else
    {
      pausar ();
      headsUpDisplay.setMensajeBotonPausaReanuda ("Jugar");
    }
  }

  /** Pausa la partida, y activa una animación de 'texto creciente'. */
  private void pausar ()
  {
    /** Pausa el hilo de juego. */
    hiloJuego.setPausa (true);
    /** Activa la animación. */
    cuadricula.setMensaje (PanelCuadricula.PAUSA);
    cuadricula.setTamanoFuenteMensaje ((byte) 20);
    cuadricula.setPintaMensaje (true);
    /** En un segundo y medio, dejarán de refrescarse constantemente las cuadrículas. */
    alarma.schedule (new java.util.TimerTask ()
    {
      public void run ()
      {
        cuadricula.setRefrescoConstante (false);
        cuadriculaSiguiente.setRefrescoConstante (false);
      }
    }, 1500);
  }

  /** arranca una animación de 'texto menguante' en la cuadrícula de juego, y despausa la partida. */
  private void despausar ()
  {
    /** Pone en marcha la animación. */
    cuadricula.setTamanoFuenteMensaje ((byte) 60);
    cuadricula.setMensaje (PanelCuadricula.DESPAUSA);
    cuadricula.setRefrescoConstante (true);
    /** En un segundo y medio: desaparecerá el texto en la cuadrícula de juego, la cuadrícula pequeña
        volverá a refrescarse todo el rato, y se despausará el hilo de juego. */
    alarma.schedule (new java.util.TimerTask ()
    {
      public void run ()
      {
        cuadricula.setPintaMensaje (false);
        if (seVeSiguientePieza)
          cuadriculaSiguiente.setRefrescoConstante (true);
        hiloJuego.setPausa (false);
      }
    }, 1500);
  }

  /** Método que convierte la siguiente pieza en la actual, y genera una nueva 'siguiente pieza'. */
  public void inicializaPiezas ()
  {
    piezaActual = siguientePieza;
    siguientePieza = piezaAleatoria ();

    /** Se vacía la cuadrícula pequeña, y se pega la 'siguiente pieza' si se tercia. */
    cuadriculaSiguiente.limpia ();
    if (seVeSiguientePieza)
      siguientePieza.pegar (cuadriculaSiguiente, true);

    /** Se intenta pegar la pieza actual a la cuadrícula de juego. Si no se puede, es 'Game Over'. */
    if (!piezaActual.pegar (cuadricula, false))
    {
      /** Antes de pasar a la función 'gameOver ()', vacía la cuadrícula pequeña, y en ella pega
          la pieza fatídica. */
      cuadriculaSiguiente.limpia ();
      piezaActual.pegar (cuadriculaSiguiente, true);
      gameOver ();
    }
  }

  /** Quita las líneas llenas, y hace los cambios correspondientes en la puntuación,
      el número de líneas completadas, y el nivel. */
  private void trataLineasLlenas ()
  {
    /** Se quitan las líneas rellenas. */
    byte lineas = cuadricula.quitaLineasLlenas ();
    /** Completar una sola línea contabiliza menos puntos proporcionalmente que hacer más. */
    if (lineas == 1)
      puntuacion += 40 * nivel * numeroReacciones;
    else
      puntuacion += lineas * 50 * nivel * numeroReacciones;
    headsUpDisplay.setPuntuacion (puntuacion);
    /** Se ajusta 'lineasCompletas', y el 'HUD' lo refleja. */
    lineasCompletas += lineas;
    headsUpDisplay.setLineasCompletas (lineasCompletas);
    /** Si toca subir el nivel, se hace. */
    if (lineasCompletas / lineasCambioNivel > nivel - offsetNivel - 1)
      subeNivel ();
  }

  /** A ejecutar cuando una pieza 'toca fondo'. Destruye las líneas llenas, si las hay, y luego
      inicializa las piezas. Debido a la 'gravedad real', y las reacciones en cadena que de ésta se
      derivan, esta función tiene un comportamiento recursivo mientras en la cuadrícula de juego
      haya líneas llenas -sólo así pueden tratarse las líneas formadas en una reacción. */
  public void piezaTocaFondo ()
  {
    if (cuadricula.hayLineasLlenas ())
    {
      /** Si hay alguna línea llena, pausa el hilo de juego y activa la animación de 'destruir líneas'. */
      hiloJuego.setPausa (true);
      cuadricula.setDestruyendoPiezas (true);
      byte lineasLlenas = cuadricula.cuentaLineasLlenas ();
      /** Si el número de líneas reventadas es mayor que 3:
            - Suena un efecto sonoro según el número de líneas completadas.
            - Y se activa una animación de 'texto creciente' con una felicitación.
          Si no lo es, tan solo suena el efecto sonoro de 'completar líneas'. */
      if (lineasLlenas > 3)
      {
        switch (lineasLlenas)
        {
          case 4:
            sonido.lineas4 ();
            break;
          case 5:
            sonido.lineas5 ();
            break;
          case 6:
            sonido.lineas6 ();
            break;
          default:
            sonido.lineas7 ();
        }
        cuadricula.setMensaje (PanelCuadricula.LINEAS);
        cuadricula.setTextoFelicitacion (cuadricula.cuentaLineasLlenas ());
        cuadricula.setTamanoFuenteMensaje ((byte) 20);
        cuadricula.setPintaMensaje (true);
        cuadricula.setPintaFelicitacion (true);
      }
      else
        sonido.lineasCompletas ();
      /** Dentro de 1.5 segundos, se harán desaparecer las líneas llenas, se quitará el texto en
          la cuadrícula, se sumará uno al nº de reacciones, y la función se llamará a si misma para
          tratar nuevas líneas llenas que han podido resultar de una reacción en cadena.*/
      alarma.schedule (new java.util.TimerTask ()
      {
        public void run ()
        {
          trataLineasLlenas ();
          cuadricula.setDestruyendoPiezas (false);
          cuadricula.setPintaMensaje (false);
          cuadricula.setPintaFelicitacion (false);
          numeroReacciones++;
          piezaTocaFondo ();
        }
      }, 1500);
    }
    else
    {
      /** Si en la cuadrícula ya no hay líneas llenas, se despausa el hilo de juego, se inicializan las
          piezas, y se reajusta el nº de reacciones en cadena. */
      hiloJuego.setPausa (false);
      inicializaPiezas ();
      numeroReacciones = 1;
    }
  }

  /** Método que genera una pieza al azar de entre las disponibles para la partida. 
      Se tiene en cuenta la frecuencia de aparición de las piezas.
      @return Pieza  : Nueva pieza aleatoria. */
  public Pieza piezaAleatoria ()
  {
    byte i;
    /** Un número real aleatorio entre 1 y 100 es generado. */
    float aleatorio = (float) (Math.random () * 100);
    /** A este 'aleatorio' se le restan las frecuencias de aparición de las piezas a medida que avanza
        el bucle. Cuando sea menor que 0, en la posición [0]['i'] de 'piezas' hay la pieza a generar. */
    for (i = 0; i < opcionesJuego.getNumeroPiezas (); i++)
    {
      aleatorio -= piezas [1][i];
      if (aleatorio < 0)
        break;
    }
    return new Pieza ((byte) piezas [0][i], disenoPieza, sonido);
  }

  /** Método para conocer la ruta absoluta del archivo con la imagen de fondo de la ventana del programa.
      @return String  : Ruta absoluta de la imagen de fondo. */
  public String getFondo ()
  {
    return fondo;
  }

  /** Define la imagen que se mostrará como fondo de ventana, y escala esa imagen para llenar
      el espacio del que dispone.
      @param nuevoFondo String  Nueva ruta absoluta de la imagen de fondo para la ventana. */
  public void setFondo (String nuevoFondo)
  {
    /** Define la cadena con la ruta de la imagen, y la imagen misma. */
    fondo = nuevoFondo;
    imagenFondo = Tetris.getImagen (nuevoFondo, "Fondo de la ventana").getImage ();
    /** Si el panel ya tiene un tamaño establecido, la imagen puede ser correctamente escalada. */
    if ((getSize ().width != 0) && (getSize ().height != 0))
      imagenFondo = imagenFondo.getScaledInstance (getSize ().width, getSize ().height,
                                                  Image.SCALE_DEFAULT);
    /** Se añade la imagen al 'MediaTracker', y hasta que éste detecta que ha sido escalada,
        pausa la ejecución del programa. */
    seguidorEstado.addImage (imagenFondo, 0);
    try
    {
      seguidorEstado.waitForID (0);
    } catch (InterruptedException e) {}
    /** Para terminar, se repinta el panel. */
    repaint ();
  }

  /** Método para conocer las imágenes disponibles para el fondo de la ventana.
      @return String[]  : Array de cadenas con los fondos de ventana disponibles. */
  public static String [] getFondosDisponibles ()
  {
    return new String [] {FONDO1, FONDO2, FONDO3, FONDO4, ""};
  }

  /** Método que permite conocer si hay una 'Pieza' en la cuadrícula de juego o no.
      @return boolean  : 'true' si hay 'Pieza' para la cuadrícula de juego, y 'false' si no. */
  public boolean hayPieza ()
  {
    return piezaActual != null;
  }

  /** Método para conocer el estado de pausa del hilo de juego.
      @return boolean  : 'true' si el hilo está pausado, y 'false' para lo contrario. */
  public boolean getPausa ()
  {
    return hiloJuego.getPausa ();
  }

  /** Función sobrecargada, útil para que los componentes de este panel se pinten correctamente.
      @param graficos Graphics  : Contexto Graphics sobre el que se dibuja. */
  public void paint (Graphics graficos)
  {
    super.paint (graficos);
    graficos.drawImage (imagenFondo, 0, 0, null);
    cuadricula.repaint ();
    headsUpDisplay.repaint ();
  }

  // Seguidamente, se sobrecargan los métodos de la interfaz 'KeyListener'.
  /** Método 'synchronized' para no interferir con el hilo de juego. 
      'keyPressed ()', además de permitirque una pulsación larga resulte en múltiples llamadas
      a ella misma, permite distinguir cada tecla individualmete, y así habilitar prácticamente
      todas las teclas del teclado para el juego.
      @param evento KeyEvent  : Evento del teclado correspondiente a la tecla pulsada. */
  public synchronized void keyPressed (KeyEvent evento)
  {
    /** Si el 'virtual key code' de la tecla que ha generado el evento corresponde al de una de
        las teclas de acción, se tomarán los pasos necesarios. */
    int tecla = evento.getKeyCode ();

    /** Primero se mira si 'tecla' corresponde a alguna acción en la que no interviene la pieza actual. */
    if (tecla == controles [6])
      pausarDespausar ();
    else if (tecla == controles [7])
      mostrarOpciones ();
    else if (tecla == controles [8])
      gameOver ();
    else if (tecla == controles [10])
    {
      if (nivel < 20)       // Si puede subir el nivel, también hay que contabilizar
        offsetNivel++;      // el incremento de 'offsetNivel'.
      subeNivel ();
    }
    else if (tecla == controles [12])
      ajustaSeVeSiguientePieza ();
    else if (tecla == controles [13])
      cuadricula.setGravedadReal (!cuadricula.getGravedadReal ());
    /** Si la partida no ha empezado, o está pausada, es inútil continuar. */
    if ((piezaActual == null) || (hiloJuego.getPausa ()))
      return;
    /** Llegado este punto, hay que ver si la tecla pulsada mueve la pieza. */
    if (tecla == controles [0])
      piezaActual.izquierda ();
    else if (tecla == controles [1])
      piezaActual.derecha ();
    else if (tecla == controles [2])
      piezaActual.abajo ();
    else if (tecla == controles [3])
      piezaActual.abajoDelTodo ();
    else if (tecla == controles [4])
      piezaActual.rota (Pieza.GIRO_HORARIO);
    else if (tecla == controles [5])
      piezaActual.rota (Pieza.GIRO_ANTIHORARIO);
  }
  /** Función sobrecargada de la interfaz 'KeyListener'. No hace nada.
      @param evento KeyEvent  : Evento del teclado correspondiente a la tecla pulsada. */
  public void keyReleased (KeyEvent evento) {}
  /** Función sobrecargada de la interfaz 'KeyListener'. No hace nada.
      @param evento KeyEvent  : Evento del teclado correspondiente a la tecla pulsada. */
  public void keyTyped (KeyEvent evento) {}


Clase HUD

  /** Clase interna que extiende 'JPanel'.  

      'Heads up display' es el nombre que recibe, en los video-juegos, el panel que contiene información
      acerca de la partida actual. 
	  
      Este panel contiene 4 botones que disparan acciones para jugar, pausar, ver las opciones,
      finalizar la partida, y terminar el programa; 3 etiquetas con la puntuación, el nivel,
      y las líneas completas; y un 'PanelCuadricula' para ver la siguiente pieza. */
  class HUD extends JPanel
  {
    // Botón para empezar la partida, pausarla, o despausarla.
    private Boton botonPausaReanuda;
    // Botón para mostrar el diálogo de inicio por el panel 'OPCIONES'.
    private Boton botonOpciones;
    // Botón para terminar la partida actual.
    private Boton botonReset;
    // Etiqueta para la puntuación de la partida.
    private Etiqueta etiquetaPuntuacion;
    // Etiqueta para el nivel de la partida.
    private Etiqueta etiquetaNivel;
    // Etiqueta para las líneas reventadas.
    private Etiqueta etiquetaLineasCompletas;

    // Un objeto gestor del diseño del panel, y otro para definir sus restricciones.
    private GridBagLayout disposicion;
    private GridBagConstraints restricciones;

    /** Constructor. Inicializa los componentes de la clase, y se los añade. */
    public HUD ()
    {
      super ();

      /** Se inicializan el gestor del diseño, y sus restricciones. */
      disposicion = new GridBagLayout ();
      restricciones = new GridBagConstraints ();
      /** Los componentes ocuparán todo el espacio del que disponen. */
      restricciones.fill = GridBagConstraints.BOTH;
      /** Se indica que cada componente ocupará una fila. */
      restricciones.gridwidth = GridBagConstraints.REMAINDER;

      /** Pone un márgen vacío, define el 'Layout', y pone un fondo blanco. */
      setBorder (new EmptyBorder (15, 20, 15, 20));
      setLayout (disposicion);
      setBackground (Color.white);

      /** Se inicializan los componentes. */
      inicializaComponentes ();
      /** Se añaden al panel. */
      add (botonPausaReanuda, restricciones);
      add (botonOpciones, restricciones);
      add (botonReset, restricciones);
      restricciones.insets = new Insets (10, 0, 0, 0);
      add (etiquetaPuntuacion, restricciones);
      restricciones.insets = new Insets (0, 0, 0, 0);
      add (etiquetaLineasCompletas, restricciones);
      add (etiquetaNivel, restricciones);
      restricciones.insets = new Insets (10, 8, 0, 0);
      add (cuadriculaSiguiente, restricciones);
    }

    /** Inicializa los componentes de clase con acciones que disparan métodos de 'PanelJuego',
        y valores de esa misma clase. La cuadrícula pequeña ya ha sido inicializada anteriormente.*/
    public void inicializaComponentes ()
    {
      botonPausaReanuda = new Boton (new AbstractAction ("Jugar")
      {
        public void actionPerformed (ActionEvent evento)
        {
          pausarDespausar ();
        }
      });
      botonOpciones = new Boton (new AbstractAction ("Opciones")
      {
        public void actionPerformed (ActionEvent evento)
        {
          mostrarOpciones ();
        }
      });
      botonReset = new Boton (new AbstractAction ("Reset")
      {
        public void actionPerformed (ActionEvent evento)
        {
          gameOver ();
        }
      });
      etiquetaPuntuacion = new Etiqueta (" Puntos: ", puntuacion);
      etiquetaLineasCompletas = new Etiqueta (" Líneas: ", lineasCompletas);
      etiquetaNivel = new Etiqueta (" Nivel: ", nivel);
    }

    /** Define la disponibilidad de los cuatro botones del 'HUD'.
        @param bool boolean  : 'true' si los botones estarán activos, y 'false' si estarán desactivados. */
    public void activaDesactivaBotones (boolean bool)
    {
      botonPausaReanuda.setEnabled (bool);
      botonOpciones.setEnabled (bool);
      botonReset.setEnabled (bool);
    }

    /** Define el texto en el botón de pausar y reanudar el juego.
        @param mensaje String  : Nueva cadena de texto para el botón. */
    public void setMensajeBotonPausaReanuda (String mensaje)
    {
      botonPausaReanuda.setText (mensaje);
    }

    /** Define el número a mostrar en la etiqueta de la puntuación.
        @param puntos long  : Nueva puntuación para la etiqueta. */
    public void setPuntuacion (long puntos)
    {
      etiquetaPuntuacion.setText (String.valueOf (puntos));
    }

    /** Define el número a mostrar en la etiqueta de la líneas completadas.
        @param lineas short  : Nuevo número de líneas completadas. */
    public void setLineasCompletas (short lineas)
    {
      etiquetaLineasCompletas.setText (String.valueOf (lineas));
    }

    /** Define el número que mostrará la etiqueta con el nivel de juego.
        @param nivel byte  : Nuevo nivel para que muestre la etiqueta. */
    public void setNivel (byte nivel)
    {
      etiquetaNivel.setText (String.valueOf (nivel));
    }

    /** Función sobrecargada que pinta el borde del panel.
        @param graficos Graphics  : Contexto 'Graphics' sobre el que pintar. */
    public void paintBorder (Graphics graficos)
    {
      graficos.setColor (Color.blue);
      graficos.drawRect (0, 0, getSize ().width - 1, getSize ().height - 1);
      graficos.drawRect (2, 2, getSize ().width - 5, getSize ().height - 5);
      graficos.drawRoundRect (10, 10, getSize ().width - 20, getSize ().height - 20, 20, 20);
    }
  }


Clase EntraRecord

  /** Clase pensada únicamente para entrar 'puntuaciones máximas' en el 'Top 10'. No tiene métodos:
      lo único que hace es mostrar un diálogo cuando se inicializa un objeto de la clase. */
  class EntraRecord
  {
    // Variables de la clase.
    // Un panel para meter como 'mensaje' en el diálogo.
    private JPanel panel;
    // Dos sub-paneles que irán dentro del anterior 'panel'.
    private PanelTitulado subPanel1;
    private PanelTitulado subPanel2;
    // Un campo de texto y una etiqueta, que irán en los 2 sub-paneles de más arriba, uno en cada uno.
    private JTextField campoTexto;
    private JLabel etiquetaPuntuacion;

    /** El constructor de esta clase inicializa el panel que irá como 'mensaje' de un 'JOptionPane',
        y a continuación hace saltar el diálogo. En éste se pide al jugador que entre su nombre
        para poder añadirlo al 'Top 10', y si el usuario pulsa el botón de 'aceptar', así lo hace.
        @param puntuacion long  : Valor de la puntuación que acaba de conseguir el jugador. Se mostrará
                                  en la etiqueta del diálogo. */
    public EntraRecord (long puntuacion)
    {
      super ();

      /** Se inicializan las variables de clase. 'etiquetaPuntuacion' se inicializa con el argumento
          del constructor. */
      panel = new JPanel ();
      subPanel1 = new PanelTitulado ("Su nombre: ");
      subPanel2 = new PanelTitulado ("Su puntuación: ");
      campoTexto = new JTextField ();
      etiquetaPuntuacion = new JLabel (String.valueOf (puntuacion));

      /** Se define el 'Layout' del 'panel', y se le añaden los 2 sub-paneles. */
      panel.setLayout (new GridLayout (2, 1));
      panel.add (subPanel1);
      panel.add (subPanel2);
      /** El 'JTextField' tendrá el texto centrado, con la fuente 'Showcard Gothic' azul, y sin borde. */
      campoTexto.setHorizontalAlignment (JTextField.CENTER);
      campoTexto.setFont (new Font ("Showcard Gothic", Font.BOLD, 20));
      campoTexto.setForeground (Color.blue);
      campoTexto.setBorder (new EmptyBorder (0, 0, 0, 0));
      /** La 'JLabel' tendrá el texto en verde, y con la misma fuente que el 'JTextField'. */
      etiquetaPuntuacion.setForeground (Color.green);
      etiquetaPuntuacion.setFont (new Font ("Showcard Gothic", Font.BOLD, 20));
      /** Se añaden a los sub-paneles el campo de texto y la etiqueta. El 'GridLayout' del
          primer sub-panel es para que el 'JTextField' ocupe todo el espacio del que dispone. */
      subPanel1.setLayout (new GridLayout ());
      subPanel1.add (campoTexto);
      subPanel2.add (etiquetaPuntuacion);

      /** Se inicializa una tabla con el texto para los botones del diálogo. Se hace visible el diálogo.
          Según qué botón pulse el usuario, su récord quedará grabado en 'opcionesJuego' o no. */
      String [] options = {"Aceptar", "Cancelar"};
      int retorno = JOptionPane.showOptionDialog (null, panel, "Ha establecido un nuevo record",
                                                 JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
                                                 null, options, options[0]);
      if (retorno == JOptionPane.OK_OPTION)
      {
        opcionesJuego.setRecord (new EnteroYCadena (puntuacion, campoTexto.getText ()));
        /** El listado del 'Top 10' en el menú de inicio también debe actualizarse. */
        panelMenuInicio.refrescaListado ();
      }
    }
  }


Clase HiloJuego

  /** Clase interna que extiende 'Thread'.
  
      Este hilo se encarga de aportar la dinámica a la partida. 
	  
      Mientras está vivo, mueve cada cierto tiempo la pieza en la cuadrícula de juego un espacio abajo.
      Proporciona métodos para pausar su ejecución, y para saber si está pausada o no. */
  class HiloJuego extends Thread
  {
    // Variables de clase. La primera decide si la ejecución del hilo está pausada o no, y la segunda
    // define el tiempo de espera que dejará pasar el hilo entre movimiento y movimiento.
    private boolean pausa;
    private short espera;

    /** Constructor por defecto. Al principio, el hilo estará pausado. */
    public HiloJuego ()
    {
      super ();
      pausa = true;
    }

    /** Metodo que arranca la ejecución del hilo, además de despausarlo y definir el tiempo de espera.
        @param nivel byte  : Valor que se utilizará para calcular el tiempo de espera del hilo. */
    public void reset (byte nivel)
    {
      ajustaEspera (nivel);
      setPausa (false);
      if (!isAlive ())
        start ();
    }

    /** Ajusta el tiempo de 'espera' entre 'movimiento' y 'movimiento' del hilo. Cuanto mayor sea
        el nivel, menor será 'espera'.
        @param nivel byte  : 'nivel' de la partida actual. */
    public void ajustaEspera (byte nivel)
    {
      espera = (short) (900 - nivel * 40);
    }

    /** Define 'pausa'.
        @param pausar boolean  : Nuevo valor de 'pausa'. */
    public void setPausa (boolean pausar)
    {
      pausa = pausar;
    }

    /** Facilita el valor de pausa.
        @return boolean  : 'true' si el hilo está en pausa, 'false' si no. */
    public boolean getPausa ()
    {
      return pausa;
    }

    /** Método sobrecargado que se llama al arrancar la ejecución del hilo.  
	
        Ejecuta un bucle interminable que, mientras no haya 'pausa', llamará a la función
        'movimiento ()', y se esperará un breve periodo de tiempo (definido por 'espera') para seguir. */
    public void run ()
    {
      while (true)
      {
        /** Mientras el hilo 'esté pausado', cederá el control de la ejecución a otros hilos. */
        while (pausa)
          try
          {
            Thread.sleep (10);
          } catch (InterruptedException e) {}

        movimiento ();

        /** A cada vuelta, el hilo se espera un poco -así, mientrastanto, pueden ejecutarse otros hilos. */
        try
        {
          Thread.sleep (espera);
        } catch (InterruptedException e) {}
      }
    }
  }
}

¿Comentarios, sugerencias?: llopsite.at.yahoo.es | © 2005-07 Albert Lobo

Última actualización: 18-Feb-2007

Hosted by www.Geocities.ws

1