Mostrar Mensajes

Esta sección te permite ver todos los posts escritos por este usuario. Ten en cuenta que sólo puedes ver los posts escritos en zonas a las que tienes acceso en este momento.


Mensajes - Kabuto

Páginas: 1 2 3 [4] 5 6 7 8 9 ... 50
61
Aprender a programar desde cero / Re: Interfaces
« en: 24 de Mayo 2023, 12:01 »
Hola.

Puedes retornar un array de char vacío así:
Código: [Seleccionar]
return new char[] {};
O también:
Código: [Seleccionar]
return new char[0]
Sin embargo, en ese método no puedes retornar ningún array char, ya que su tipo es CharSequenceTokio
Citar
CharSequenceTokio subSequence(int start, int end);

Así que has de devolver un objeto de tipo CharSequenceTokio.

Pero, la cosa se complica un poco más, porque CharSequenceTokio es solo una interfaz, en su interior no contiene ningún array de char.
Por lo tanto, lo que has de retornar es algún tipo de clase que sí contenga como atributo un array de char, y además, implemente la interfaz CharSequenceTokio

Por tanto, lo que puedes retornar es un objeto de la clase ArrayCharSequenceTokio, que es la misma clase donde estás implementando dicho método.
A este objeto le pasarás por su constructor un array de char vacío en caso de igualdad de índices, o un array de con una secuencia de char si los índices de inicio y fin son correctos.

Te dejo aquí como he escrito yo la clase ArrayCharSequenceTokio por si quieres verla, aunque antes lo ideal es que hagas tú un par de intentos por tu cuenta a ver si lo consigues:
Código: [Seleccionar]
public class ArrayCharSequenceTokio implements CharSequenceTokio {

private char[] secuencia;

public ArrayCharSequenceTokio(char[] secuencia) {
this.secuencia = secuencia;
}

@Override
public int length() {
return secuencia.length;
}

@Override
public char charAt(int index) {
if (index < 0 || index >= length())
return '0';
else
return secuencia[index];
}

@Override
public CharSequenceTokio subSequence(int start, int end) {
//Start no valido
if (start < 0 || start >= length())
return null;
//End no valido
if (end < 0 || end >= length())
return null;
//Start mayor que end
if (start > end)
return null;
//Start es igual que end
if (start == end) {
return new ArrayCharSequenceTokio(new char[0]);//Array sin caracteres
}
//Indices válidos
char[] subSecuencia = new char[end+1 - start];
for (int i = start; i < end; i++)
subSecuencia[i] = secuencia[i];

return new ArrayCharSequenceTokio(subSecuencia);
}

@Override
public String toString() {
StringBuilder cadena = new StringBuilder();
for (char caracter: secuencia)
cadena.append(caracter);

return cadena.toString();
}

}

Y aquí una clase main() de prueba para comprobar su funcionamiento.
Fíjate que para obtener la subsecuencia, es necesario hacer un casting, ya que el tipo del método es CharSequenceTokio(interfaz) pero el objeto con el que se puede trabajar es ArrayCharSequenceTokio(clase)
Código: [Seleccionar]
public class Prueba {

public static void main(String[] args) {

Scanner teclado = new Scanner(System.in);
ArrayCharSequenceTokio secuencia = new ArrayCharSequenceTokio(new char[] {'v','e','l','o','z'});

System.out.println("Secuencia original: " + secuencia);

System.out.print("\nElija una posicion de la secuencia entre 0 y " + (secuencia.length()-1) +  ": ");
int c1 = teclado.nextInt();
System.out.println("Caracter obtenido: " + secuencia.charAt(c1));

System.out.println("\nElija ahora posiciones de inicio y fin para hacer una subcadena.");
System.out.print("Inicio: ");
int start = teclado.nextInt();
System.out.print("Fin: ");
int end = teclado.nextInt();

ArrayCharSequenceTokio subCadena = (ArrayCharSequenceTokio) secuencia.subSequence(start, end);
if (subCadena == null)
System.out.println("Indices no validos");
else {
System.out.println("Subcadena obtenida: " + subCadena);
System.out.println("Longitud de subcadena: " + subCadena.length());
}

System.out.println("\n\t\tFIN DE PROGRAMA");
teclado.close();
}

}

Un saludo.

63
Eso es.
Todo método que tenga algún tipo de retorno, será una "función"(computa algo y retorna un resultado).
Si no retorna nada (void), entonces es un "procedimiento"(hace una serie de tareas sin retornar resultado).

En Java y otros lenguajes basados en Programación Orientada a Objetos (POO) normalmente se les llama a todos simplemente "métodos" sin hacer ninguna distinción en cuanto a si retornan algo o no.

Pero está bien saber hacer esta diferenciación de cara a otros lenguajes donde sí es costumbre hacer esta distinción.

Un saludo.

64
Hola.

De esas signaturas, quizás habría que corregir la tercera.

En esta caso, se supone que son métodos que se van a aplicar a un disco en concreto. Es decir, no sería necesario recibir el nombre del disco como argumento:
Citar
public float getDuracionDisco(String tituloDisco)

...porque como digo, ese método se aplica a un disco en concreto.
Sería distinto si se tratase de un método destinado a actuar en una "colección de discos", entonces sí se requeriría saber el nombre del disco sobre el que queremos actuar.


Otra cosa a mencionar, de poca importancia, es que se aconseja evitar usar la letra ñ.
En programación es habitual que compartas tu código con otras personas o que tengas que portarlo a distintos compiladores, editores de texto, etc.... y estos no siempre van a estar configurados para reconocer caracteres ajenos a los alfabetos anglosajones.
Eso puede hacer que el programa no se compile, o también que se muestren caracteres extraños si se ejecuta en un sistema que no contemple diccionarios españoles.

Por eso mejor evitar usar la ñ, las vocales con tilde u otros caracteres como pueden ser ¿ ¡ ç ...


Ah, por cierto. De esos métodos que has escrito, ¿sabes indicar cuáles son "procedimientos" y cuáles "funciones"?

Un saludo.

65
Comunidad / Re: Presentación
« en: 21 de Mayo 2023, 20:05 »
Encantados de recibirte.
Eres libre de participar, preguntar y responder.

Un saludo.

66
Aprender a programar desde cero / Re: Ejercicio cartas java
« en: 14 de Mayo 2023, 21:06 »
El toString() pertenece a la clase Mazo y no hay que modificarlo.
Lo que hace es mostrar el mazo de cartas, sin importar si está ordenado o no.

Para completar la clase OrdPalNumDec, basta con copiar el código de OrdPalNumInc y, en el método ordena(), cambiamos el comparador de "mayor que" por el de "menor que".

Citar
   public void ordena(List listaCartas) {
      // Algoritmo de ordenación
      for(int i = 0;i < listaCartas.size()-1;i++){
         for(int j = 0;j < listaCartas.size()-i-1;j++){
            int compara = comparadorCarta((Carta)listaCartas.get(j), (Carta)listaCartas.get(j+1));
            if (compara < 0) {
               //Intercambiamos posiciones
               Object aux = listaCartas.get(j+1);
               listaCartas.set(j+1, listaCartas.get(j));
               listaCartas.set(j, aux);
            }
         }
      }
   }

De esta manera se invierte el orden, que es lo queremos, y si ahora modificamos el main para que también se muestre esta ordenación, veremos que el mazo se imprime de tres formas distintas sin tener que modificar el toString().
Citar
public class Main {

   public static void main(String[] args) {
      
      Baraja baraja = new Baraja();
        Mazo mazo = baraja.getMazo();
        System.out.println("Baraja original: ");
        System.out.println(mazo.toString());

        mazo.setAlgoritmo(new OrdPalNumInc());
        mazo.ordena();
        System.out.println("\nOrdenado por palo y numero incrementando: ");
        System.out.println(mazo.toString());

       
        mazo.setAlgoritmo(new OrdPalNumDec());
        mazo.ordena();
        System.out.println("\nOrdenado por palo y numero decrementando: ");
        System.out.println(mazo.toString());
       
        /*
        mazo.setAlgoritmo(new OrdNumIncPal());
        mazo.ordena();
        System.out.println(mazo.toString());*/

   }

}


Falta completar OrdNumIncPal, en este caso hay que cambiar más el código.
En los dos anteriores se ordenaba primero por palo, y luego por número.

En esta caso, se ordena primero por número(valor de carta) y luego los palos.
Es decir, primero se mostraran los ases de cada palo, luego el 2 de cada palo, el 3, etc...

Entonces, para esta clase, el método ordena() si es idéntico al de la clase OrdPalNumInc, porque se encarga de ordenar de manera incremental, sea lo que sea lo que determine el método comparadorCarta().

Así que es el método comparadorCarta() el que ha de cambiar el código, porque ahora hay que dar preferencia a ordenar por el número de carta. Y si los número de las cartas que se comparan son iguales, entonces se ordenará por palo.
Código: [Seleccionar]
    }

    public static int comparadorCarta(Carta c1, Carta c2) {
    if (c1.equals(c2)) //Cartas son iguales
        return 0;
    else {
    if (c1.getNumero() == c2.getNumero()) //Mismo número, se ordenará por palo
    return c1.getPalo().compareTo(c2.getPalo());
    else {//Distinto número de carta
    //Averiguamos el valor de estas cartas según posición en el array
    int posiC1 = 0, posiC2 = 0;
        for (int i = 0; i < Baraja.numeros.length; i++) {
        if (c1.getNumero().equals(Baraja.numeros[i]))
        posiC1 = i;
        if (c2.getNumero().equals(Baraja.numeros[i]))
        posiC2 = i;
        }
        //Tenemos sus posiciones numéricas, podemos compararlas para decidir el orden.
        if (posiC1 == posiC2)
        return 0; //Número es igual
        else if (posiC1 < posiC2)
        return -1; //c1 es menor que c2
        else
        return 1; //c2 es menor que c1
    }
   
    }
    }

Y ahora sí, en el Main activamos todo el código para mostrar la baraja sin ordenar y ordenada de tres formas distintas.
Es decir, se muestran 4 configuraciones distintas de la baraja, y repito, todas sin necesidad de alterar el método toString().


Solo han de cambiar los métodos ordena()(decide si el orden es incremental o decremental) y comparadorCarta()(decide si tiene preferencia ordenar por valor u ordenar por palo) de las tres clases que heredan de AlgoritmoOrdenacion

Espero se hayan aclarado tus dudas, si no, no dudes en repreguntar.

Saludos.

67
A ver si puedo ayudarte, porque Pseint apenas lo conozco. Cuando yo estudié programación, hace ya siglos, esto no existía y el pseudocódigo lo escribíamos con papel y lápiz. Lo compilábamos en nuestro cerebro xD, y nunca lo veíamos ejecutarse realmente.


Para intentar detectar qué está ocurriendo, puedes añadir líneas de código que te ayuden a saber por donde está discurriendo realmente la ejecución del programa.
En Pseint tienes un modo ejecución paso a paso que puede ayudar a depurar el programa, pero muchas veces es más rápido e intuitivo añadir temporalmente una línea de código con algún mensaje en pantalla que te indique por donde está fluyendo el código.

Por ejemplo, se supone que si introduces 4 cifras, el programa debería fluir por la "alternativa" que hay después del primer SiNo, ¿verdad?
Pues vamos a comprobar si realmente eso está ocurriendo, añadiendo un mensaje en pantalla cuando el código discurra por esa alternativa

Citar
Si (N >= 1 y n <= 99999) Entonces
      N4 <- N MOD 10
      N <- trunc(N/10)
      N3 <- N MOD 10
      N <- trunc(N/10)
      N2 <- N MOD 10
      N <- trunc(N/10)
      N1 <- N MOD 10
      N <- trunc(N/10)
      N0 <- N MOD 10
      N <- trunc(N/10)
      Si (N1 > 0) y (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
         NC <- N4 * 10000 + N3 * 1000 + N2 * 100 + N1 * 10 + N0
      SiNo
         Si (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
            Escribir "4 cifras introducidas"
            NC <- N4 * 1000 + N3 * 100 + N2 * 10 + N1

         SiNo
            Si (N3 > 0) y (N4 > 0) Entonces
               NC <- N4 * 100 + N3 * 10 + N2
            SiNo
               Si (N4 > 0) Entonces
                  NC <- N4 * 10 + N3
               SiNo
                  NC <- N4
               FinSi
            FinSi
         FinSi
      FinSi   

Si lo ejecutamos, oh sorpresa, ese mensaje no aparece en pantalla. El programa no está discurriendo por donde esperábamos.
A ver, pongamos mensajes similares en todas las alternativas posibles.
Citar
      Si (N1 > 0) y (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
         Escribir "5 cifras introducidas"
         NC <- N4 * 10000 + N3 * 1000 + N2 * 100 + N1 * 10 + N0
      SiNo
         Si (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
            Escribir "4 cifras introducidas"
            NC <- N4 * 1000 + N3 * 100 + N2 * 10 + N1
         SiNo
            Si (N3 > 0) y (N4 > 0) Entonces
            Escribir "3 cifras introducidas"
               NC <- N4 * 100 + N3 * 10 + N2
            SiNo
               Si (N4 > 0) Entonces
                  Escribir "2 cifras introducidas"
                  NC <- N4 * 10 + N3
               SiNo
                  Escribir "1 cifra introducida"
                  NC <- N4
               FinSi
            FinSi
         FinSi
      FinSi   

Si probamos distintas longitudes de cifra, vemos algo curiosos.
Si el números es de 5 o de 4 cifras, el código va por el mismo sitio.
Si es de 3 cifras, lo identifica como si fueran 4 cifras.
Si es 2 cifras, lo identifica como si fuera 3
Si es de 1, para el programa son 2...

Algo ocurre que hace que no se evalúe correctamente la longitud de cifras.

Y seguramente es que no estamos haciendo las preguntas correctas.

Llama un poco la atención el que para preguntar si es un número de 5 cifras, solo estamos evaluando 4 variables (N1,N2,N3,N4).
Y para preguntar si es de 4 cifras, solo evaluamos 3 (N2,N3,N4)
Para preguntar si es de 3 cifras, solo se evalúan 2 (N3,N4)
Y para saber si es de 2, solo se evalúa N4.

Quizás deberíamos preguntar por el mismo número de variables que las cifras que nos interesan.
Para preguntar por 5 cifras, vamos a incluir a N0 -> (N0,N1,N2,N3,N4).
Para preguntar por 4 cifras, incluyamos a N1 -> (N1,N2,N3,N4).
Y así sucesivamente...
Citar
      Si (N0 > 0) y (N1 > 0) y (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
         Escribir "5 cifras introducidas"
         NC <- N4 * 10000 + N3 * 1000 + N2 * 100 + N1 * 10 + N0
      SiNo
         Si (N1 > 0) y (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
            Escribir "4 cifras introducidas"
            NC <- N4 * 1000 + N3 * 100 + N2 * 10 + N1
         SiNo
            Si (N2 > 0) y (N3 > 0) y (N4 > 0) Entonces
               Escribir "3 cifras introducidas"
               NC <- N4 * 100 + N3 * 10 + N2
            SiNo
               Si (N3 > 0) y (N4 > 0) Entonces
                  Escribir "2 cifras introducidas"
                  NC <- N4 * 10 + N3
               SiNo
                  Escribir "1 cifra introducida"
                  NC <- N4
               FinSi
            FinSi
         FinSi
      FinSi 

Si lo ejecutamos, veremos que ahora sí identifica correctamente la longitud de cifras  ;D
Así que este era el problema, ya puedes quitar los mensajes en pantalla que hemos introducido, o también puedes dejarlos si quieres.

Por cierto, para preguntar si es de 1 cifra, se podría preguntar por N4 para continuar con la misma lógica que se ha aplicado con las otras alternativas, pero no es necesario.

Al principio de todo ya se ha preguntado si N es mayor que 0. Y si esto se cumple, ya está garantizado que N4 también será mayor que 0. Así que no hace falta preguntarlo, sería redundante.
Se podría quitar de también N4 de las otras preguntas alternativas y funcionaría igual.

Sin embargo, el programa no es perfecto, hay problemas con la cifra 0

Si yo introduzco 01234, dice que son 4 cifras y publica 4321.
Esto es totalmente correcto, sin problema aquí.

Pero, si introducimos por ejemplo 12340, aquí vienen los problemas.

Ese es un número de 5 cifras, no hay duda, pero ese 0 confunde al programa y la cosa no va bien
Código: [Seleccionar]
*** Ejecución Iniciada. ***
ESCRIBE LOS NUMEROS QUE QUIERES INVERTIR
> 12340
1 cifra introducida
EL NUMERO EN INVERSO ES 0
*** Ejecución Finalizada. ***

Si quitamos (N4 >0) de las preguntas de los Si Entonces, mejora un poco el resultado, pero sigue sin ser correcto
Código: [Seleccionar]
*** Ejecución Iniciada. ***
ESCRIBE LOS NUMEROS QUE QUIERES INVERTIR
> 12340
5 cifras introducidas
EL NUMERO EN INVERSO ES 4321
*** Ejecución Finalizada. ***

Y de todos modos, si el 0 estuviera en medio, la cosa va a peor
Código: [Seleccionar]
*** Ejecución Iniciada. ***
ESCRIBE LOS NUMEROS QUE QUIERES INVERTIR
> 12034
2 cifras introducidas
EL NUMERO EN INVERSO ES 43
*** Ejecución Finalizada. ***

Bien, este problema creo que no tiene solución, si seguimos intentando solucionarlo creando un nuevo valor entero con todas las cifras de forma invertida, que es lo que estamos haciendo en la variable NC.

He probado varias formas de hacerlo y ninguna termina de funcionar bien del todo.
Y es porque no hay forma de insertar un 0 en mitad de un valor entero de forma sencilla.
Es una limitación por la forma en que funcionan los valores enteros.
Podría hacerse tal vez de alguna forma supercomplicada, con montones de Si Entonces anidados, para decidir si una cifra conviene multiplicarla por 10, o por 100, o por 1000.... para añadir un cero extra en el valor entero.

Si cambiamos el enfoque y no intentamos crear un nuevo valor entero, encontraremos formas más sencillas de resolverlo.
Una sería tratar las cifras como caracteres y concatenarlas en una cadena con la función CONCATENAR()
Pero posiblemente aún no la conozcas ni te dejen usarla.

Otra posibilidad es ir escribiendo en pantalla directamente cada cifra que conseguimos al hacer la operación N MOD 10

Para que salgan juntas en la misma línea, podemos usar la instrucción Escribir Sin Saltar (que supongo si podrás usarla...)

Cada vez que escribimos una cifra, hacemos el truncado y comprobamos si N sigue siendo mayor que 0.
Si esto es cierto, es que todavía podemos obtener cifras, por lo que de nuevo haremos MOD 10, Escribir sin Saltar y el truncado.
Si no fuese cierto, es decir, si N ha alcanzado el valor 0, significa que ya no quedan cifras por obtener del valor introducido, así que no haremos nada y el programa terminará.

Este sería el código.
Fíjate que de esta manera, solo necesitamos dos valores enteros para resolverlo.
Código: [Seleccionar]
Algoritmo NUMEROS_EN_INVERSOS
//INGRESE UN NUMERO DE CINCO CIFRAS DE TALK FORMA QUE SE
//REPORTE EN FORMA INVERTIDA
//EJEMPLO: 12345 REPORTE: 54321
Definir N, cifra Como Entero
ESCRIBIR "ESCRIBE LOS NUMEROS QUE QUIERES INVERTIR"
LEER N
Si (N >= 1 y N <= 99999) Entonces

cifra <- N MOD 10
Escribir Sin Saltar cifra
N <- trunc(N/10)

Si (N > 0)
cifra <- N MOD 10
Escribir Sin Saltar cifra
N <- trunc(N/10)
FinSi

Si (N > 0)
cifra <- N MOD 10
Escribir Sin Saltar cifra
N <- trunc(N/10)
FinSi

Si (N > 0)
cifra <- N MOD 10
Escribir Sin Saltar cifra
N <- trunc(N/10)
FinSi

Si (N > 0)
cifra <- N MOD 10
Escribir Sin Saltar cifra
FinSi

SiNO
Escribir "NO SE PUEDE INVERTIR ESE NUMERO "
FinSi
Escribir " "
FinAlgoritmo

Esta vez si lo ejecutamos, veremos que podemos invertir sin problema valores que tengan alguna cifra 0 al final o en medio.
Incluso si tienen varias cifras 0
Código: [Seleccionar]
*** Ejecución Iniciada. ***
ESCRIBE LOS NUMEROS QUE QUIERES INVERTIR
> 10203
30201
*** Ejecución Finalizada. ***

De todos modos, tu solución una vez corregida, está muy bien.
Solo tendría el problema de las cifras 0, pero como he dicho, es inevitable si intentamos solucionarlo construyendo un nuevo entero.

Un saludo.

68
Hola Alberto.

No domino Pseint así que poco puedo ayudarte. Incluso desconocía que con Pseint se pudiera hacer POO.

El caso es que este mismo ejercicio lo estuvimos viendo hace un par de años, pero para escribirlo en lenguaje Java, diseñando una interfaz gráfica e incluso grabando datos de usuarios en disco.

Puedes verlo en este enlace, por si a caso te sirve de ayuda, al menos para tener una idea de por donde empezar y qué clases modelar.
Mucho de lo que hay escrito es para el diseño de la interfaz gráfica, que requiere mucho código y de hecho no llegué a terminarlo :-\

Pero en cualquier caso, la lógica del programa está ahí desarrollada.
Aunque no conozcas Java, posiblemente puedas ver la lógica que se ha seguido y adaptarla a tus conocimientos de Pseint.

Un saludo.

69
por qué introduces los cinco botones dentro de JPanel, que a su vez se introducen en el JPanel PanelBotones en lugar de colocar los botones directamente sobre el JPanel PanelBotones.
Es para maquetar un poco mejor los botones.
En ese panel, estoy colocando los elementos con un GridLayout.
GridLayout tiene un característica de doble filo, hace que todos los componentes que contiene se maximicen para ocupar el máximo espacio posible del contenedor que está maquetando.

Si colocamos directamente los botones, estos crecerán hasta llenar el panel por completo.
Puedes comprobarlo, comenta las líneas originales y pon líneas donde se añadan directamente los botones.
Citar
   public PanelBotones(int ancho, int alto) {
      btIniciar = new MiBoton("Iniciar", ancho);
      btAcelerar = new MiBoton("Acelerar", ancho);
      btAcelerar.setEnabled(false);
      btFrenar = new MiBoton("Frenar", ancho);
      btFrenar.setEnabled(false);
      btDetenContinuar = new MiBoton("Detener/Continuar", ancho);
      btDetenContinuar.setEnabled(false);
      btReset = new MiBoton("Reset", ancho);
      btReset.setEnabled(false);
      setPreferredSize(new Dimension(ancho, alto));
      setLayout(new GridLayout(5, 1));
      /*add(new PanelBoton(btIniciar));
      add(new PanelBoton(btAcelerar));
      add(new PanelBoton(btFrenar));
      add(new PanelBoton(btDetenContinuar));
      add(new PanelBoton(btReset));*/

      add(btIniciar);
      add(btAcelerar);
      add(btFrenar);
      add(btDetenContinuar);
      add(btReset);

   }
Verás que el aspecto de los botones cambia:


Y bueno, la verdad es que en este caso tampoco quedan mal así tan grandes.
Pero la mayoría de las veces vamos a querer que tengan un tamaño "normal". La forma de conseguirlo es "envolverlos" con un JPanel.
De esta manera, lo que se amplia es el JPanel, no los botones.

Si usamos un JPanel tal como viene por defecto, puede servir. Pero entonces, no solo supone demasiado código porque hay que declarar un JPanel para cada botón:
Citar
      setPreferredSize(new Dimension(ancho, alto));
      setLayout(new GridLayout(5, 1));
      JPanel pn1 = new JPanel();
      pn1.add(btIniciar);
      JPanel pn2 = new JPanel();
      pn2.add(btAcelerar);
      JPanel pn3 = new JPanel();
      pn3.add(btFrenar);
      JPanel pn4 = new JPanel();
      pn4.add(btDetenContinuar);
      JPanel pn5 = new JPanel();
      pn5.add(btReset);
      add(pn1);
      add(pn2);
      add(pn3);
      add(pn4);
      add(pn5);

Si no que además los botones se van a colocar en la parte superior del panel que le está envolviendo. Porque por defecto los JPanel se maquetan con FlowLayout.
Eso hará que el primer botón se vea muy cerca del marco superior y el último botón en cambio, se verá muy alejado del marco inferior.


Para ahorrar código y que además los botones queden centrados en los paneles que les envuelven, podemos crear una clase hija de JPanel y establecer ahí una maquetación más adecuada:
Citar
      setLayout(new GridLayout(5, 1));
      add(new PanelBoton(btIniciar));
      add(new PanelBoton(btAcelerar));
      add(new PanelBoton(btFrenar));
      add(new PanelBoton(btDetenContinuar));
      add(new PanelBoton(btReset));
   }
   
   private class PanelBoton extends JPanel {

      public PanelBoton(JButton boton) {
         setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
         boton.setAlignmentX(Component.CENTER_ALIGNMENT);
         add(Box.createVerticalGlue());
         add(boton);
         add(Box.createVerticalGlue());
      }

   }

En esa clase estamos diciendo que el JPanel se maquete con BoxLayout en vertical, es decir, que los elementos se vayan colocando de arriba a abajo.
Con setAligmentX le estamos diciendo que el botón ha de estar en el centro respecto al eje horizontal, de lo contrario se pegará a la izquierda del panel.
No hay forma de decirle también se coloque en el centro respecto al eje vertical, por narices se colocará arriba del panel.
Pero eso lo solucionamos añadiendo dos "cajas" con Box.createVerticalGlue().
Estas "cajas" ocuparán todo el espacio que puedan, dejando lo necesario para que el botón se coloque entre ellas. Así el botón quedará perfectamente centrado dentro de su panel.

Y en la interfaz veremos que los botones están centrados, y que hay el mismo espacio entre el marco superior y el marco inferior.

Cita de: manuromero73
Por otro lado por qué usar pack() y por qué usar setLocationRelativeTo(null).

pack() lo que hace es dar automáticamente al JFrame las dimensiones necesarias para respetar los preferredSize() de todos los componentes que componen la interfaz, ya sean paneles, botones, campos, etc..
Digamos que sustituye al método setSize() con el que le diríamos al JFrame exactamente que tamaño queremos que tenga. Habrá veces que nos interese darle unas dimensiones concretas y exactas.
Pero en este caso, como ya le hemos dicho a cada panel sus "tamaños preferidos", podemos usar pack() y el respetará y sumará esos "tamaños preferidos" para que el JFrame pueda contenerlos.

setLocationRelativeTo() es para indicarle en que posición queremos que aparezca el marco.
Podríamos decirle por ejemplo que se colocara encima, debajo, etc... de otro marco que ya estuviera en pantalla, si fuera el caso.
Pero si le damos valor null como argumento, entonces lo que hace es colocarlo en el centro de la pantalla, que es lo que nos va a interesar la mayoría de las veces.

Cita de: manuromero73
Por otro lado lo que yo entiendo es que SwingUtilities.invokeLater(new Runnable() lo que hace es crear un hilo de ejecucion, no sé si es así y también me gustaria preguntar qué pasa si no creas un hilo de ejecución.

Sí, más o menos.
Si las aplicaciones Swing las ejecutamos en el hilo principal del programa, puede ocurrir que si ese hilo se mantiene ocupado por otra tarea, entonces la interfaz se quede como bloqueada y sin responder durante el tiempo que esa otra tarea está ocupando el hilo principal.

Por ejemplo, a lo mejor el programa está haciendo una conexión online para descargar algo de una base de datos, o bien está haciendo unos cálculos con enormes matrices de tipo double que requiere muchos ciclos de computación.
Y mientras esto ocurre el usuario notará que los botones tardan en hacer caso, que el texto que está tecleando tarda mucho en aparecer en pantalla y que la interfaz en general no está respondiendo.

Para evitar eso, se suele trasladar la aplicación Swing a un segundo hilo que de hecho se crea automáticamente (no lo creamos nosotros), al hilo del Event Dispatcher(Despachador de Eventos). Este es el hilo que está atento a cuando ocurre un evento: un click de ratón, un tecla pulsada, un desplegable se abre para mostrar una lista de elementos,...
Este hilo en realidad tiene poca cosa que hacer, así que al mismo tiempo que "escucha" posibles eventos también puede encargarse de actualizar los cambios que ocurren en la interfaz. Ya que de hecho, normalmente la interfaz solo cambia cuando previamente ocurre un evento. Así que se encargará de escuchar el evento y acto seguido actualizar la interfaz según el evento que haya ocurrido.

Por tanto, es costumbre lanzar la aplicación Swing como un Runnable() con esta instrucción para que use el hilo del Event Dispatcher y no dependa el hilo principal que puede verse "ahogado" por otras tareas.
Código: [Seleccionar]
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});

En realidad, la mayoría de las veces el programa va a funcionar igual aunque no hagamos esto.
Porque de hecho, los programadores ya saben qué tareas pueden ahogar el hilo principal y normalmente las lanzaran en otros hilos.

En este programa por ejemplo, lo de mover la bola es una tarea algo pesada, porque lo que estamos haciendo en realidad es repintar un panel varias veces por segundo.
Este proceso lo estamos realizando dentro de un SwingWorker que como ya dije, lo que hace es crear un nuevo hilo para esta tarea.
Por tanto, nuestro programa consta de tres hilos(Threads): el principal, el del Event Dispatcher y el que estamos creando nosotros con el SwingWorker.

Así que podríamos ejecutar la interfaz tranquilamente en el hilo principal sin tener que derivarla al hilo del Event Dispatcher, porque el trabajo "duro" ya lo hace el SwingWorker.

Pero bueno, ya casi que a modo de "buenas costumbres", siempre mejor derivar la interfaz al hilo del Event Dispatcher, incluso aunque sea una aplicación sencilla o las tareas pesadas estén controladas con otros hilos.

Un saludo.

70
Aprender a programar desde cero / Re: Ejercicio cartas java
« en: 24 de Abril 2023, 18:49 »
A ver, por el nombre de esta clase:
OrdPalNumInc
Supongo que se han de ordenar primero por el Palo, y luego por el Numero, de forma "incremental". Es decir, los palos en orden lexicográfico (parecido al orden alfabético) AAA,AAB,ABA,ABB,ABC, etc.... y los números por orden de valor numérico: 1(as), 2,3,4, ...., hasta el 12(rey)

Ordenar por palos no es problema, pues es comparar Strings, que Java ya sabe como debe hacerlo.
Pero por números, es más complicado, porque no son Integers. También son Strings:
Citar
numeros ={"AS", "DOS", "TRES", "CUATRO",
            "CINCO", "SEIS", "SIETE", "SOTA", "CABALLO", "REY"};
Así que hay que busca la forma de decirle al programa que el String AS es más pequeño que el String DOS.
Y que el String DOS es más pequeño que el String TRES, etc...
Bueno, eso lo veremos luego.

Volvamos  a la clase OrdPalNumInc
De momento, nos dan esto para completar:
Código: [Seleccionar]
public class OrdPalNumInc extends AlgoritmoOrdenacion{

@Override
    public void ordena(List listaCartas) {
        // Algoritmo de ordenación
    }

    public static int comparadorCarta(Carta c1, Carta c2) {
        // Comparador de cartas usado en el algoritmo de ordenación
        return 0;
    }

}

Primero habría que desarrollar el método comparadorCarta(), que es donde vamos escribir la lógica para que las cartas se comparen de forma que primero se ordenen según el palo, y si resulta que ambas cartas son del mismo palo, entonces se ordenan según el número.

La lógica sería:
* Si ambas cartas son exactamente iguales, se retorna 0.

* Si ambas cartas tienen el mismo palo, el orden depende de los números
   - Si los números fueran iguales, se retorna 0. En realidad esto ya está contemplado
    en el punto anterior.(Cartas exactamente iguales).
    - Si la carta c1 tiene un número inferior al de c2, se retorna -1.
    - Si la carta c2 tiene un número inferior al de c1, se retorna 1.

* Si las cartas tienen distinto palo, el número no influye. Hay que hacer una comparación lexicográfica de los nombres de los palos para decidir quién va primero al ordenar.
Las comparaciones lexicográficas son un poco complejas, pero la clase String ya sabe como hacerlas, así que se la pediremos a ella y retornaremos lo que ella diga.

Bien, esta lógica no es difícil. Solo está el problema al comparar los nombres de los números, ya que lo que necesitamos es su valor aritmético, pero el atributo "número" es un String. Sin embargo, podemos apoyarnos en el array de la clase Baraja donde están declarados los nombres de los números:
Código: [Seleccionar]
public static final String[] numeros ={"AS", "DOS", "TRES", "CUATRO",
            "CINCO", "SEIS", "SIETE", "SOTA", "CABALLO", "REY"};
Ahí están declarados en el orden correcto. Así que por ejemplo la posición que ocupa el String "AS" que es 0, nos sirve para saber que es menor que el String "DOS" el cuál ocupa la posición 1
Así que nos basta con obtener la posición que cada nombre de número ocupa en el array y hacer con ellos una comparación aritmética.

Quizás se entienda mejor leyendo el código:
Código: [Seleccionar]
    public static int comparadorCarta(Carta c1, Carta c2) {
        if (c1.equals(c2)) //Cartas son iguales
        return 0;
        else {
        if (c1.getPalo().equals(c2.getPalo())) { //Mismo palo, el orden dependerá de los números
        /*
        * El atributo "numero" de las Cartas es un String, no es un Integer.
        * Esto significa que no podemos comparar directamente esos atributos, porque en realidad
        * no son números, y necesitamos lograr un orden aritmético.
        *
        * Por suerte, la clase Baraja tiene un array público con los nombres de estos "números"
        * en el orden correcto.
        * Así que podemos usar la posición que ocupa cada "nombre de número" en el array para obtener
        * unos valores enteros con los que poder comparar de forma aritmética
        */
        int posiC1 = 0, posiC2 = 0;
        for (int i = 0; i < Baraja.numeros.length; i++) {
        if (c1.getNumero().equals(Baraja.numeros[i]))
        posiC1 = i;
        if (c2.getNumero().equals(Baraja.numeros[i]))
        posiC2 = i;
        }
        //Tenemos sus posiciones numéricas, podemos compararlas para decidir el orden.
        if (posiC1 == posiC2)
        return 0; //Número es igual
        else if (posiC1 < posiC2)
        return -1; //c1 es menor que c2
        else
        return 1; //c2 es menor que c1
        }
        else
        return c1.getPalo().compareTo(c2.getPalo());//Retornamos la comparación por defecto de Strings
        }

Ok, tenemos el método que dice como se han de compara las cartas.
Ahora hay que hacer el método ordena() que, valiéndose de estas reglas de comparación, ordene el List de Cartas.

Podemos aplicar el método de la burbuja para ordenar. Lo que haremos será comparar cartas según el algoritmo que hemos escrito antes.
Si el resultado de comparar resulta ser un valor mayor que 0, significa que la segunda carta es menor que la primera y por tanto han de intercambiar las posiciones.
Código: [Seleccionar]
@Override
    public void ordena(List listaCartas) {
        // Algoritmo de ordenación
for(int i = 0;i < listaCartas.size()-1;i++){
        for(int j = 0;j < listaCartas.size()-i-1;j++){
        int compara = comparadorCarta((Carta)listaCartas.get(j), (Carta)listaCartas.get(j+1));
        //Solo hay que intercambiar posiciones si la segunda carta es menor
        //es decir, si el valor de compara es positivo
        //System.out.println(compara);
        if (compara > 0) {
        //Intercambiamos posiciones
        Object aux = listaCartas.get(j+1);
        listaCartas.set(j+1, listaCartas.get(j));
        listaCartas.set(j, aux);
        }
        }
}
    }

Vale, pues en principio la clase OrdPalNumInc ya está completada. Habría que ponerla a prueba.
Para ello, primero hay que ir a la clase Mazo y completar un par de métodos.
Son muy sencillos, un setter para setear el algoritmo de ordenación que se quiere aplicar y el otro método es para ordenar que el algoritmo aplicado ordene las cartas:
Citar
public class Mazo {
   
       List cartas;
       private AlgoritmoOrdenacion algoritmo;

       public void setAlgoritmo(AlgoritmoOrdenacion algoritmo) {
           this.algoritmo = algoritmo;
       }

       public void ordena() {
          if (algoritmo == null)
             System.out.println(
"No se ha establecido ningún algoritmo de ordenación");
          else
             algoritmo.ordena(cartas);

       }

Y ahora sí, podemos poner a prueba el algortimo.
Primero podemos poner como comentario en la clase Main las líneas donde se aplicarían los algoritmos que aún no hemos escrito:
Citar
public class Main {

   public static void main(String[] args) {
      
      Baraja baraja = new Baraja();
        Mazo mazo = baraja.getMazo();
        System.out.println(mazo.toString());

        mazo.setAlgoritmo(new OrdPalNumInc());
        mazo.ordena();
        System.out.println(mazo.toString());

        /*
        mazo.setAlgoritmo(new OrdPalNumDec());
        mazo.ordena();
        System.out.println(mazo.toString());
       
        mazo.setAlgoritmo(new OrdNumIncPal());
        mazo.ordena();
        System.out.println(mazo.toString());*/

   }

}

Y ahora sí, al ejecutar el programa, veremos en pantalla las cartas del mazo original y a continuación las cartas ordenadas tal y como se esperaba.
Ordenadas primero por el palo, y luego por el número, en orden incremental.

Ahora faltaría completar la clase OrdPalNumDec, que es como la que hemos hecho pero invirtiendo el resultado de las comparaciones ya que el orden es decremental (de mayor a menor)

Y la clase OrdNumIncPal, que supongo que primero se han de ordenar por número, y luego por palo, en orden incremental.

En la clase AlgoritmoOrdenacion no hay que hacer nada de nada, ya que es una clase únicamente pensada para ser la "madre" de las otras tres clases de algoritmos.
De hecho, lo lógico habría sido declararla como una interface, en lugar de como una clase.
Pero bueno, este es el código que nos han dado y hay que respetarlo.  ;D

Otra cosa que no me gusta son todos esos List "sin tipar".
Código: [Seleccionar]
public void ordena(List listaCartas) {
}

Carajo, si ya sabemos el tipo de dato que van a contener estos List, pues pongámoslo para que todo quede bien parametrizado:
Citar
public void ordena(List<Carta> listaCartas) {
   }

Pero bueno, si no han querido ponerlo, pues nada...
Lo importante es que se entienda la lógica de estos ejercicios.

Intenta completar esas dos clases que faltan, y si te atascas o tienes dudas, avisa por aquí y te ayudamos a terminarlo.

Un saludo.

71
Nos faltaban los botones para consultar datos de ocupación, recaudación, etc...

Es sencillo.
Hacemos una nueva clase que herede de JPanel en el package "vista".

Le maquetaremos 4 botones en vertical.
Para poder escribir sus ActionListener en esta misma clase, en lugar de hacerlo en la clase Main, lo que haremos será que por constructor reciba una referencia a la matriz de las butacas, ya que es lo único que se necesita para poder hacer las consultas que nos piden.

Así, teniendo a mano la matriz, en esta misma clase podemos hacer los 4 ActionListener donde cada uno se ocupará de una de las tareas.
Código: [Seleccionar]
package vista;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import modelo.Butaca;

public class PanelBotones extends JPanel {

private MiBoton btOcupacion;
private MiBoton btRecaudacion;
private MiBoton btDisponibilidadTipo;
private MiBoton btListaDisponibles;
private Butaca[][] butacas;

public PanelBotones(Butaca[][] butacas) {

btOcupacion = new MiBoton("Ocupacion");
btOcupacion.addActionListener(new AccionOcupacion());
btRecaudacion = new MiBoton("Recaudacion");
btRecaudacion.addActionListener(new AccionRecaudacion());
btDisponibilidadTipo = new MiBoton("Tipos Disponibles");
btDisponibilidadTipo.addActionListener(new AccionDisponibilidadTipos());
btListaDisponibles = new MiBoton("Lista Disponibles");
btListaDisponibles.addActionListener(new AccionListarDisponibles());
this.butacas = butacas;

setLayout(new GridLayout(4, 1));
add(new PanelBoton(btOcupacion));
add(new PanelBoton(btRecaudacion));
add(new PanelBoton(btDisponibilidadTipo));
add(new PanelBoton(btListaDisponibles));

setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createRaisedSoftBevelBorder(),
BorderFactory.createEmptyBorder(20,20,20,20)));
setBackground(new Color(223, 195, 188));
}

private class PanelBoton extends JPanel {

public PanelBoton(JButton boton) {
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
boton.setAlignmentX(Component.CENTER_ALIGNMENT);
add(Box.createVerticalGlue());
add(boton);
add(Box.createVerticalGlue());
setBackground(new Color(223, 195, 188));
}
}

public class MiBoton extends JButton {
public MiBoton(String txt) {
super(txt);
setFont(new Font("Verdana", Font.PLAIN, 16));
}
}

//Acciones de los botones
private class AccionOcupacion implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {

int ocupadas = 0;

for (int i = 0; i < butacas.length; i++)
for (int j = 0; j < butacas[i].length; j++)
if (!butacas[i][j].estaLibre())
ocupadas++;

float ocupacion = ocupadas * 100 / 42;

JOptionPane.showMessageDialog(null, String.format("Porcentaje de ocupación: %.2f%%", ocupacion));
}
}

private class AccionRecaudacion implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {

float recaudacion = 0;

for (int i = 0; i < butacas.length; i++)
for (int j = 0; j < butacas[i].length; j++)
if (!butacas[i][j].estaLibre())
recaudacion += butacas[i][j].getPrecio();

JOptionPane.showMessageDialog(null, String.format("Total Recaudacion: %.2f$", recaudacion));
}
}

private class AccionDisponibilidadTipos implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {

int general = 0, VIP = 0;

for (int i = 0; i < butacas.length; i++)
for (int j = 0; j < butacas[i].length; j++)
if (butacas[i][j].estaLibre())
if (butacas[i][j].getTipo()==0)
general++;
else
VIP++;

JOptionPane.showMessageDialog(null,
String.format("Disponibilidad por tipos.\nGeneral: %d\nVIP: %d", general, VIP));
}
}

private class AccionListarDisponibles implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {

StringBuilder mensaje = new StringBuilder("Butacas libres.");

for (int i = 0; i < butacas.length; i++) {
mensaje.append("\n");
for (int j = 0; j < butacas[i].length; j++)
if (butacas[i][j].estaLibre())
mensaje.append(butacas[i][j].getId()  + " ");
}

JOptionPane.showMessageDialog(null, mensaje);
}
}

}

Y ahora solo queda agregar este panel a la interfaz de la clase Main
Citar
public class Main extends JFrame {
   
   private GestorCine gestor;
   private PanelSala sala;
   private PanelLogin login;
   private PanelBotones botones;
   private Cliente cliente;
   
   public Main() {
      
      gestor = new GestorCine();
      sala = new PanelSala(gestor.getButacas(), new AccionReservar());
      login = new PanelLogin();
      login.btLogin.addActionListener(new AccionLogin());
      botones = new PanelBotones(gestor.getButacas());
      cliente = null;
      
      setLayout(new BorderLayout());
      add(new PanelTitulo("Cinema Poli"), BorderLayout.NORTH);
      add(sala, BorderLayout.CENTER);
      add(login, BorderLayout.SOUTH);
      add(botones, BorderLayout.EAST);


Y ya tenemos botones funcionando.


Adjunto zip con el código final.
Hay detalles que se pueden pulir, como mejorar el mensaje al listar las butacas disponibles. Si no hay ninguna libre estaría bien que dijera "No hay butacas disponibles" en lugar de salir un lista en blanco.

Pero eso ya que cada uno lo arregle a su gusto.

Un saludo.

72
A ver, hagamos un planteamiento nuevo.

Se mostrarán las 72 butacas en pantalla y el usuario podrá hacer click en ellas para reservar, o liberar reservas.

Previamente, el usuario se ha de registar si es nuevo o identificarse si ya está registrado.

Podemos hacer que las butacas cambien entre tres colores distintos.
VERDE = Butaca disponible
ROJO = Butaca reservada, por un cliente DISTINTO al que está logueado en ese momento
AZUL = Butaca reservada, por el cliente ACTUALMENTE logueado.

Así, el cliente no puede interactuar con las casillas rojas.
Solo podrá clickar las verdes para reservar, o las azules para deshacer la reserva.

Podemos estructurar el proyecto en tres packages: vista, modelo y controlador
Un cuarto package podría contener las dos imágenes que usaremos para mostrar las butacas.

Veamos primero el "Modelo".
Primero, la clase Butaca.
Su atributo "tipo" lo controlamos con un simple int que puede variar entre 0(general) y 1(VIP)
De esta forma, nos sirve como índice para seleccionar directamente en un array uno de los dos precios según el tipo.

Con un String le construiremos un id, que será un número de butaca. Este id servirá para mostrarlo en pantalla pero también para hacer que cada Butaca sea única.

Luego un boolean para saber si está libre u ocupada.

Por constructor recibe el id y el tipo. El boolean se inicia como true, ya que al principio del programa todas las butacas estarán libres.
Código: [Seleccionar]
package modelo;

public class Butaca {
//Precios prefijados según tipo de Butaca
private final float[] PRECIOS = {8.5f, 10f};

private String id;
private int tipo; //Tipo Butaca. 0 = General / 1 = VIP
private boolean estaLibre; //True está libre, False está ocupada

public Butaca(String id, int tipo) {
this.id = id;
this.tipo = tipo;
estaLibre = true;
}

public String getId() {
return id;
}

public int getTipo() {
return tipo;
}

public boolean estaLibre() {
return estaLibre;
}

public float getPrecio() {
return PRECIOS[tipo];
}

public void setLibre(boolean esLibre) {
estaLibre = esLibre;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Butaca) {
Butaca otraBt = (Butaca) obj;
return id.equals(otraBt.id);
}
else
return false;
}
}

Veamos ahora la clase Cliente.
Puesto que nos dicen que los clientes pueden hacer reservas, pero también anularlas, lo más cómodo y rápido para saber que butacas pertenecen a cada cliente es hacer que la clase Cliente tenga un ArrayList con las butacas que vaya reservando.
Así podemos añadir o quitar reservas.

Otros atributo será un número de cedula y su nombre completo para poder dirigirnos a él.
Entre sus métodos habrá uno que se encargue de calcular el importe total de sus reservas
Código: [Seleccionar]
package modelo;

import java.util.ArrayList;

public class Cliente {

private String cedula;
private String nombreCompleto;
public ArrayList<Butaca> reservas;

public Cliente(String cedula, String nombreCompleto) {
this.cedula = cedula;
this.nombreCompleto = nombreCompleto;
reservas = new ArrayList<Butaca>();
}

public String getCedula() {
return cedula;
}
public String getNombreCompleto() {
return nombreCompleto;
}

public void hacerReserva(Butaca bt) {
reservas.add(bt);
}

public void liberarReserva(Butaca bt) {
reservas.remove(bt);
}

public float calculaImporte() {
float total = 0;
for(Butaca bt: reservas)
total += bt.getPrecio();
return total;
}

}

Bueno, en lo que respecta a la parte del "Controlador", podemos crear una clase para gestionar las dos clases del "Modelo" que acabamos de ver.
Esta clase tendrá un ArrayList para poder añadir clientes y una matriz con las 42 butacas divididas entre 6 filas y 7 columnas.

En su constructo, al inicializar cada objeto Butaca, le generamos un identificador concatenando el valor de su fila con el de su columna.
Además, decidiremos cuáles van a ser VIP. Podemos hacer que las centrales, las que están en las filas 2 y 3, sean VIP y el resto normales.
Esto ya que cada uno decida como quiere hacerlo.

Tiene un método para añadir un nuevo cliente, y otro para buscar un cliente mediante su número de cédula y retornarlo en caso de que exista.

También hay un getter para retornar la matriz de butacas al completo. Esto facilitará las cosas luego.
Código: [Seleccionar]
package controlador;

import java.util.ArrayList;

import modelo.*;

public class GestorCine {

private ArrayList<Cliente> clientes;
private Butaca[][] butacas;

public GestorCine() {
clientes = new ArrayList<Cliente>();
butacas = new Butaca[7][6];
//Inicializamos butacas. Filas centrales serán VIP
for(int i = 0; i < 7; i++)
for(int j = 0; j < 6; j++)
if(i == 2 || i == 3) //Filas centrales
butacas[i][j] = new Butaca(String.format("%d%d",i,j), 1);//VIP
else
butacas[i][j] = new Butaca(String.format("%d%d",i,j),0);//General
}

public void nuevoCliente(Cliente cli) {
clientes.add(cli);
}

public Cliente compruebaCliente(String cedula) {
//Comprobamos por cedula si ya existe este cliente
for (Cliente cl: clientes) {
if(cl.getCedula().equals(cedula))
return cl;
}
//Bucle finaliza sin retorna nada, no existe este cliente
return null;
}

public Butaca[][] getButacas() {
return butacas;
}

}

Sobre la "Vista"...
Queremos mostrar la imagen de una butaca, que será distinta según su tipo.
Además queremos rodear cada imagen con un borde de color para distinguir fácilmente las butacas ocupadas de las que están libres.

Vale, pues podemos crear una clase que herede de JPanel y llamarla PanelButaca.
Cada uno de estos paneles, recibirá una referencia a una de las 72 Butacas con la que estará asociada.
Así sabrá qué número de butaca ha de mostrar, cuál color ha de ser su borde en cada momento y también qué imagen ha de mostrar según el tipo de butaca.

El color del borde también lo vamos a gestionar como un atributo más.

Así que a este JPanel le vamos a añadir dos JLabel en vertical.
Uno mostrará el número de butaca y el otro la imagen jpg que corresponda a su tipo de butaca.
Código: [Seleccionar]
package vista;

import java.awt.Color;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;

import modelo.Butaca;

public class PanelButaca extends JPanel {
//Según tipo butaca, se cargará una de las dos imagenes
private final String[] RUTAS_IMAGEN = new String[] {
"img/butaca_general.jpg", "img/butaca_VIP.jpg"
};

private Butaca butaca;
private Color color;

public PanelButaca(Butaca butaca) {
this.butaca = butaca;
color = Color.GREEN;
setBackground(Color.WHITE);

setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
JLabel idButaca = new JLabel(butaca.getId());
idButaca.setFont(new Font("Consolas", Font.BOLD, 18));
idButaca.setForeground(Color.BLUE);
idButaca.setBorder(BorderFactory.createEmptyBorder(3, 3, 0, 20));
add(idButaca);

JLabel lbButaca = new JLabel(new ImageIcon(
this.getClass().getClassLoader().getResource(RUTAS_IMAGEN[butaca.getTipo()])));
lbButaca.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
add(lbButaca);
cambiaBorde(color);
setToolTipText(setTip());
}

public Butaca getButacaAsociada() {
return butaca;
}

public Color getColor() {
return color;
}

private String setTip() {
if (butaca.estaLibre())
return "Precio: " + butaca.getPrecio();
else
return "Ocupada";
}

public void cambiaBorde(Color color) {
this.color = color;
setBorder(BorderFactory.createMatteBorder(10,10,10,10, color));
setToolTipText(setTip());
}

}

Ahora necesitamos otro panel, que contenga a estos 72 PanelButaca, como si fuera la sala de cine, así que se puede llamar PanelSala

También tendrá una matriz de 7x6 para contener los 72 PanelButaca.
Estos PanelesButaca requieren de las otras 72 Butacas del "Modelo" para inicializarse.
Así que por el constructor le haremos llegar la matriz del "Modelo" para poder crear la matriz de la "Vista.

Por constructor también recibirá un MouseListener que será quien le diga a cada PanelButaca cómo ha de comportarse cuando el usuario haga click en ellas.

De momento, le vamos a dar dos métodos.

Uno de ellos recibe como argumento el cliente logueado y se va a encargar de marcar en azul las butacas reservadas por ESTE cliente.
Para ello cotejaremos el ArrayList de reservas que posee este cliente para saber cuáles hay que marcar en azul.

El otro método, se ejecutará cuando el cliente cierre sesión, y lo que hará será repasar todas las butacas y marcarlas en verde o rojo.
El color azul solo estará presente cuando haya un cliente con la sesión abierta.
Código: [Seleccionar]
package vista;

import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.MouseListener;

import javax.swing.BorderFactory;
import javax.swing.JPanel;

import modelo.*;

public class PanelSala extends JPanel {

private PanelButaca[][] butacas;

public PanelSala(Butaca[][] listaButacas, MouseListener escuchador) {

butacas = new PanelButaca[7][6];

setLayout(new GridLayout(7,6,2,4));
for (int i = 0; i < 7; i++)
for (int j = 0; j < 6; j++) {
butacas[i][j] = new PanelButaca(listaButacas[i][j]);
butacas[i][j].addMouseListener(escuchador);
add(butacas[i][j]);
}
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(10, 10, 10, 10),
BorderFactory.createBevelBorder(0)));
}

public void mostrarReservasCliente(Cliente cl) {
//Comprobamos cuáles butacas estan reservadas por este cliente
//para marcarlas en azul
for (int i = 0; i < 7; i++)
for (int j = 0; j < 6; j++)
if (cl.reservas.contains(butacas[i][j].getButacaAsociada()))
butacas[i][j].cambiaBorde(Color.BLUE);
}

public void actualizaColorButacas() {
for (int i = 0; i < 7; i++)
for (int j = 0; j < 6; j++)
if (butacas[i][j].getButacaAsociada().estaLibre())
butacas[i][j].cambiaBorde(Color.GREEN);
else
butacas[i][j].cambiaBorde(Color.RED);
}

}

Bien, ¿y como hará login un cliente?
Podemos añadir un panel en la parte inferior de la interfaz con un único botón que servirá para hacer Login y Logout.
Irá acompañado de un JLabel que irá mostrando en todo momento cuántas reservas tiene hechas el cliente y el importe total que suman.
Código: [Seleccionar]
package vista;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class PanelLogin extends JPanel {

public JButton btLogin;
public JLabel mensaje;

public PanelLogin() {
btLogin = new JButton("Login");
JPanel pnBoton = new JPanel();
pnBoton.add(btLogin);

mensaje = new JLabel("Pulsa el botón para iniciar sesión");
JPanel pnMensaje = new JPanel();
pnMensaje.add(mensaje);

setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
add(pnBoton);
add(pnMensaje);

setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(5, 10, 5, 10),
BorderFactory.createBevelBorder(1)));
}

}

En la parte superior, pondremos otro panel con el nombre del cine.
No voy a invertir mucho esfuerzo en esto, con algo simple ya basta.
Código: [Seleccionar]
package vista;

import java.awt.Color;
import java.awt.Font;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class PanelTitulo extends JPanel {

public PanelTitulo(String titulo) {
JLabel lbTitulo = new JLabel(titulo);
lbTitulo.setForeground(Color.MAGENTA);
lbTitulo.setBackground(Color.WHITE);
lbTitulo.setOpaque(true);
lbTitulo.setFont(new Font("Impact", Font.BOLD, 40));
lbTitulo.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEtchedBorder(Color.MAGENTA, Color.CYAN),
BorderFactory.createEmptyBorder(20, 45, 20, 45)));
add(lbTitulo);
}

}

Bien, con estos elementos ya podemos poner en marcha una versión preliminar del programa.
Para ello nos vamos al "controlador" y creamos una clase Main, la principal, que heredará de JFrame. Mostrará la interfaz, comunicará unas clases con otras y aquí escribiremos los Listener que podamos necesitar para cada función del programa.

Sus atributos serán los principales paneles, el gestor del cine y también un objeto Cliente que contendrá al cliente que haya iniciado sesión.
Si no hay ninguna sesión abierta, tendrá valor null.

Escribiremos un ActionListener para el botón de iniciar sesión.
Ahí se puede ver como tendrá dos comportamientos distintos, según si hay una sesión abierta o está cerrada.

También un MouseListener que será él que le haremos llegar a cada uno de los 72 PanelesButaca
Mediante este Listener es con el que el usuario podrá hacer reservas con solo clickar una butaca.
Por tanto, el comportamiento dependerá de si clicka en una casilla verde, azul, o roja.

Es en esta clase donde hay que prestar más atención, pues es donde se decide la mayor parte de la lógica del programa.
Código: [Seleccionar]
package controlador;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import modelo.*;
import vista.*;

public class Main extends JFrame {

private GestorCine gestor;
private PanelSala sala;
private PanelLogin login;
private Cliente cliente;

public Main() {

gestor = new GestorCine();
sala = new PanelSala(gestor.getButacas(), new AccionReservar());
login = new PanelLogin();
login.btLogin.addActionListener(new AccionLogin());
cliente = null;

setLayout(new BorderLayout());
add(new PanelTitulo("Cinema Poli"), BorderLayout.NORTH);
add(sala, BorderLayout.CENTER);
add(login, BorderLayout.SOUTH);

setTitle("Cinema Poli");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}

private class AccionLogin implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {

if (login.btLogin.getText().equals("Login")) {
String cedula = JOptionPane.showInputDialog("Introduzca su cedula:");
cliente = gestor.compruebaCliente(cedula);
if (cliente == null) {
String nombre = JOptionPane.showInputDialog("Complete el nuevo registros con su nombre completo:");
cliente = new Cliente(cedula, nombre);
gestor.nuevoCliente(cliente);
}
JOptionPane.showMessageDialog(null, "Bienvenido " + cliente.getNombreCompleto() +
"\nLas casillas ROJAS indican que la butaca está reservada por otro cliente."
+ "\nLas VERDES indican que están disponibles."
+ "\nLas AZULES son las que tienes reservadas a tu nombre.");
sala.mostrarReservasCliente(cliente);
login.mensaje.setText(String.format("Hola %s. Tienes %d reservas. Import total: %.2f",
cliente.getNombreCompleto(), cliente.reservas.size(), cliente.calculaImporte()));
login.btLogin.setText("Logout");
}
else {
int confirma = JOptionPane.showConfirmDialog(null, "¿Cerrar sesión?", "Logout", JOptionPane.YES_NO_OPTION);
if (confirma == JOptionPane.YES_OPTION) {
cliente = null;
sala.actualizaColorButacas();
login.mensaje.setText("Pulsa el botón para iniciar sesión");
login.btLogin.setText("Login");
}
}
}
}

private class AccionReservar implements MouseListener {

@Override
public void mouseClicked(MouseEvent e) {
if (cliente == null)
JOptionPane.showMessageDialog(null, "Primero ha de iniciar sesión con el boton Login");
else {

PanelButaca bt = (PanelButaca)e.getSource();

if (bt.getButacaAsociada().estaLibre()) {
bt.getButacaAsociada().setLibre(false);
bt.cambiaBorde(Color.BLUE);
cliente.hacerReserva(bt.getButacaAsociada());
login.mensaje.setText(String.format("Hola %s. Tienes %d reservas. Importe total: %.2f",
cliente.getNombreCompleto(), cliente.reservas.size(), cliente.calculaImporte()));
}
else {
//Si no está libre, solo actuamos si su color es azul, porque es una reserva del cliente
//Al hacer click en su reserva, la butaca quedará liberada
if (bt.getColor().equals(Color.BLUE)) {
bt.getButacaAsociada().setLibre(true);
bt.cambiaBorde(Color.GREEN);
cliente.liberarReserva(bt.getButacaAsociada());
login.mensaje.setText(String.format("Hola %s. Tienes %d reservas. Importe total: %.2f",
cliente.getNombreCompleto(), cliente.reservas.size(), cliente.calculaImporte()));
}
}
}
}
@Override
public void mousePressed(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Main();
}
});
}
}

Si ejecutamos, veremos que podemos iniciar sesión con número de cedula.
Si somos nuevos, el sistema nos registrará.
Si ya estamos registrados, el sistema nos recuerda y nos mostrará las butacas que tenemos reservadas actualmente.

La interfaz no es bonita, pero cumple con su trabajo


Ahora faltaría añadir otro panel con botones para poder consultar las otras cosas que se solicitan
Citar
se debe contar con un menú (botones) para genera y mostrar la siguiente información: porcentaje de ocupación del cine, total de dinero recaudado,
número de sillas disponibles según su tipo (general o VIP), qué sillas se encuentran disponibles, especificando la fila y la columna de cada silla en este estado.

Pero esto tendrá que ser otro día.

Por cierto, adjunto un zip con mis packages del proyecto.
Un saludo.

73
Pues efectivamente, la solución ha sido establecer un valor fijo para una de las "desviaciones".
Así una desviación decide el ángulo de rebote y la otra la velocidad en píxeles, que será constante.

Lo que sí podremos seguir controlando desde GestorJuego son los milisegundos que transcurren entre cada repintado del tablero, variando así la sensación de velocidad.

En la clase Bola lo que he hecho es añadir una constante y se irá aplicando a una u otra de las dos desviaciones, según donde ocurra la colisión y cambiando a positivo o negativo según corresponda.

Código: [Seleccionar]
public class Bola extends Ellipse2D {

private int posX;
private int posY;
private double ancho;
private double alto;
//Atributos necesario para calcular colisiones y rebotes
private int desviacionX;
private int desviacionY;
private final int LIMITE_X;
private final int LIMITE_Y;
private Random azar;

final int VELOCIDAD;

public Bola(int x, int y, int w, int h, int limiteX, int limiteY) {
posX = x;
posY = y;
ancho = w;
alto = h;
desviacionX = 0;
desviacionY = 0;
//Los límites se calculan restando las dimensiones de la bola a las dimensiones del tablero
LIMITE_X = (int) (limiteX - ancho);
LIMITE_Y = (int) (limiteY - alto);
azar = new Random();
VELOCIDAD = 5; //Una de las desviaciones ha de tener siempre ese valor
}

public void mover() {
//Antes de mover, comprobamos si la bola está detenida (desviaciones = 0)
//Si está detenida, calcularemos nuevas desviaciones.
//Si no, la bola se moverá según las desviaciones ya existentes
if (desviacionX == 0 && desviacionY == 0) {
//Una desviación se calcula al azar, para decidir el ángulo
//Otra usará el valor de velocidad
desviacionX = VELOCIDAD;
desviacionY = azar.nextInt(6);
//Elegimos al azar si son negativas o positivos
if (azar.nextBoolean())
desviacionX *= -1;
if (azar.nextBoolean())
desviacionY *= -1;
}
//El movimiento consiste en alterar la posicion según las "desviaciones"
posX += desviacionX;
posY += desviacionY;
}

public void detener() {
desviacionX = 0;
desviacionY = 0;
}

public void comprobarColision() {
//Comprobamos si hemos alcanzado algún limite
//en cuyo caso calcularemos nuevas desviaciones según donde hayamos colisionado

//Colisión en el borde oeste
if (posX <= 0) {
//A la desviación de X le damos el valor de velocidad
desviacionX = VELOCIDAD;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}

//Colisión en el borde este
if (posX >= LIMITE_X) {
//La desviacion de X ha de ser negativa
desviacionX = VELOCIDAD * -1;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}

//Colisión en el borde norte
if (posY <= 0) {
//La desviacion de Y será la velocidad
desviacionY = VELOCIDAD;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}

//Colisión en el borde sur
if (posY >= LIMITE_Y) {
//La desviacion de Y ha de ser negativa
desviacionY = VELOCIDAD * -1;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}
}

@Override
public Rectangle2D getBounds2D() {
Rectangle2D rect = new Rectangle2D.Double(posX, posY, ancho, alto);
return rect.getBounds2D();
}
@Override
public double getX() {
return posX;
}
@Override
public double getY() {
return posY;
}
@Override
public double getWidth() {
return ancho;
}
@Override
public double getHeight() {
return alto;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void setFrame(double x, double y, double w, double h) {

}

}

Y eso es todo, ahora ya estaría funcionando el programa.

Lo último que he cambiado también, es el aspecto de los botones.
Ya los he centrado mejor y ampliado la fuente con un valor relativo según el ancho del panel.
Código: [Seleccionar]
public class PanelBotones extends JPanel {

public MiBoton btIniciar;
public MiBoton btAcelerar;
public MiBoton btFrenar;
public MiBoton btDetenContinuar;
public MiBoton btReset;

public PanelBotones(int ancho, int alto) {
btIniciar = new MiBoton("Iniciar", ancho);
btAcelerar = new MiBoton("Acelerar", ancho);
btAcelerar.setEnabled(false);
btFrenar = new MiBoton("Frenar", ancho);
btFrenar.setEnabled(false);
btDetenContinuar = new MiBoton("Detener/Continuar", ancho);
btDetenContinuar.setEnabled(false);
btReset = new MiBoton("Reset", ancho);
btReset.setEnabled(false);
setPreferredSize(new Dimension(ancho, alto));
setLayout(new GridLayout(5, 1));
add(new PanelBoton(btIniciar));
add(new PanelBoton(btAcelerar));
add(new PanelBoton(btFrenar));
add(new PanelBoton(btDetenContinuar));
add(new PanelBoton(btReset));
}

private class PanelBoton extends JPanel {

public PanelBoton(JButton boton) {
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
boton.setAlignmentX(Component.CENTER_ALIGNMENT);
add(Box.createVerticalGlue());
add(boton);
add(Box.createVerticalGlue());
}
}

public class MiBoton extends JButton {
public MiBoton(String txt, int ancho) {
super(txt);
//Tamaño fuente se calcula según el ancho del panel
float sizeFuente = getFont().getSize() * ancho * 0.7f /100;
setFont(getFont().deriveFont(sizeFuente));
}
}

}


En este vídeo puede verse el programa en funcionamiento:
https://www.youtube.com/watch?v=H59d522WDxM

Además adjunto un zip con el código fuente final, por si alguien se ha perdido con tantos cambios en la clases.


Ha sido interesante este ejercicio, no estaba seguro de si me iba a salir o no.
Espero que se entienda toda la lógica que he seguido. Preguntad lo que haga falta.

Y sería maravilloso si alguien propusiera una solución distinta. Seguro que hay alguna forma más eficiente y correcta de hacer esto.

Un saludo.

74
OK, más avances.
Bueno, en realidad lo he terminado, pero hay un detalle que no ha quedado bien respecto a la velocidad de la bola que luego explicaré.

He de decir que aunque he usado las clases que pedía el enunciado, no he usado ninguno de los métodos que se proponían. El camino que he ido siguiendo me ha llevado a un enfoque distinto.

Dije que la clase GestorJuego iba a tener el código más complicado porque era donde se iba a calcular colisiones, rebotes, y tal....
Pues bueno, al final el código no solo ha resultado ser menos complicado de lo que pensaba, si no que además al final es la propia clase Bola quien controla si ha colisionado y decide hacia donde debe rebotar.

Para que la bola pueda hacer esto, le he dado nuevos atributos.
Unos de esos atributos son los limites para X e Y, es decir, tanto para un eje como para el otro, la bola puede ir desde 0 hasta un límite.
Esos límites serán las dimensiones del tablero, pero restándole las dimensiones de la propia bola.
Es decir, si el tablero de ancho mide 500 pixeles, y el ancho de la bola es 10 pixeles, pues el límite será 490. De lo contrario, la bola se saldría del tablero antes de rebotar.

Otros de esos atributos, son las "desviaciones" de X e Y. Estas desviaciones son las que deciden la dirección y ángulo en la que la bola se mueve.

Son valores int que comienzan con valor 0 (bola no se mueve) y luego adoptan valores al azar entre 1 y 5, o bien entre -1 y -5. Es decir, pueden ser positivos o negativos.

Estas desviaciones son las cantidades en las que se incrementan o decrementan las posiciones X e Y de la bola. Así el movimiento se dará en distintos ángulos y direcciones.

Por tanto, tendremos un método mover() que lo que hará es aplicar estas desviaciones a X e Y para dar sensación de movimiento cada vez que se repinte el tablero.
Si la bola estuviera en reposo, se calcularán unas desviaciones al azar, de lo contrario, se aplicarán las desviaciones que estén vigentes.

Tendremos otro método llamado detener() para poner a 0 estas desviaciones con lo que la bola pasa a estado de reposo.

Y luego, el método más importante, será el llamado comprobarColision().
Este método comprueba si la bola ha colisionado con alguno de los cuatro bordes del tablero.
Dependiendo de con cuál borde ha colisionado, calculará nuevas desviaciones de distintas formas.
Por ejemplo, si colisiona con la parte superior, el norte, significa que el eje Y tenía una desviación negativa y tras la colisión obligatoriamente ha de pasar a positiva para ahora dirigirse hacia el sur
El eje X en cambio, ha de conservar la misma dirección, sea este u oeste.

Clase Bola
Código: [Seleccionar]
public class Bola extends Ellipse2D {

private int posX;
private int posY;
private double ancho;
private double alto;
//Atributos necesario para calcular colisiones y rebotes
private int desviacionX;
private int desviacionY;
private final int LIMITE_X;
private final int LIMITE_Y;
private Random azar;

public Bola(int x, int y, int w, int h, int limiteX, int limiteY) {
posX = x;
posY = y;
ancho = w;
alto = h;
desviacionX = 0;
desviacionY = 0;
//Los límites se calculan restando las dimensiones de la bola a las dimensiones del tablero
LIMITE_X = (int) (limiteX - ancho);
LIMITE_Y = (int) (limiteY - alto);
azar = new Random();
}

public void mover() {
//Antes de mover, comprobamos si la bola está detenida (desviaciones = 0)
//Si está detenida, calcularemos nuevas desviaciones.
//Si no, la bola se moverá según las desviaciones ya existentes
if (desviacionX == 0 && desviacionY == 0) {
//Calculamos nuevas desviaciones al azar para empezar a mover
desviacionX = azar.nextInt(6);
desviacionY = azar.nextInt(6);
//Elegimos al azar si son negativas o positivos
if (azar.nextBoolean())
desviacionX *= -1;
if (azar.nextBoolean())
desviacionY *= -1;
}
//El movimiento consiste en alterar la posicion según las "desviaciones"
posX += desviacionX;
posY += desviacionY;
}

public void detener() {
desviacionX = 0;
desviacionY = 0;
}

public void comprobarColision() {
//Comprobamos si hemos alcanzado algún limite
//en cuyo caso calcularemos nuevas desviaciones según donde hayamos colisionado

//Colisión en el borde oeste
if (posX <= 0) {
//La desviacion de X ha de ser positiva y mayor que 0
desviacionX = azar.nextInt(5) + 1;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}

//Colisión en el borde este
if (posX >= LIMITE_X) {
//La desviacion de X ha de ser negativa
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
//Desviacion de Y varía, pero ha de mantener la misma dirección actual
if (desviacionY < 0) {
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
}
else
desviacionY = azar.nextInt(5) + 1;
}

//Colisión en el borde norte
if (posY <= 0) {
//La desviacion de Y ha de ser positiva
desviacionY = azar.nextInt(5) + 1;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}

//Colisión en el borde sur
if (posY >= LIMITE_Y) {
//La desviacion de Y ha de ser negativa
desviacionY = azar.nextInt(5) + 1;
desviacionY *= -1;
//Desviacion de X varía, pero ha de mantener la misma dirección actual
if (desviacionX < 0) {
desviacionX = azar.nextInt(5) + 1;
desviacionX *= -1;
}
else
desviacionX = azar.nextInt(5) + 1;
}
}

@Override
public Rectangle2D getBounds2D() {
Rectangle2D rect = new Rectangle2D.Double(posX, posY, ancho, alto);
return rect.getBounds2D();
}
@Override
public double getX() {
return posX;
}
@Override
public double getY() {
return posY;
}
@Override
public double getWidth() {
return ancho;
}
@Override
public double getHeight() {
return alto;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void setFrame(double x, double y, double w, double h) {

}

}

Hemos dicho que clase Bola ahora conoce sus límites de rebote. Estos límites, se los hacemos llegar por constructor y es la clase Tablero quien se los proporciona.
Clase Tablero
Código: [Seleccionar]
public class Tablero extends JPanel {

private int ancho;
private int alto;
private Bola bola;

public Tablero(int ancho, int alto) {
this.ancho = ancho;
this.alto = alto;
bola = new Bola(ancho/2, alto/2,
ancho/30, alto/25, ancho, alto);
setPreferredSize(new Dimension(ancho, alto));
setBorder(BorderFactory.createEtchedBorder(0));
setBackground(Color.WHITE);
}

public int getAncho() {
return ancho;
}

public int getAlto() {
return alto;
}

public Bola getBola() {
return bola;
}

public void reset() {
bola = new Bola(ancho / 2, alto / 2,
ancho / 30, alto / 25, ancho, alto);
repaint();
}

@Override
public void paint(Graphics g) {
super.paint(g);
        Graphics2D G2D = (Graphics2D)g;
        //Limpiamos panel con un rectangulo blanco
        G2D.setColor(Color.WHITE);
        G2D.fillRect(0, 0, ancho, alto);
        //Ahora pintamos bola
        G2D.setColor(Color.BLUE);
        G2D.fill(bola);
        G2D.draw(bola);
}

}

Veamos ahora la clase GestorJuego.
Como dije al principio, esta clase ha quedado liberada de tener que calcular colisiones y rebotes, ya que es la propia Bola quien se encarga.

Pero sigue teniendo un papel importante.
Dentro de esta clase he creado un SwingWorker que es quien pondrá en marcha el movimiento de la bola.
Un SwingWorker es como un Thread, es decir, sirve para crear una hilo de ejecución separado del hilo principal. Pero es una versión mejorada y pensada para usar cuando hemos creado una interfaz con Swing, ya que a veces un Thread normal puede interferir con el "despachador de eventos", es decir, el hilo que se encarga de comprobar si en la interfaz Swing pulsamos un botón, o seleccionamos un campo de texto, o redimensionamos una ventana, etc...

Este SwingWorker lo que hará será:
-pedir a la bola que se mueva
-pedir a la bola que compruebe si ha colisionado
-repintar el tablero para que se refleje el cambio de posición de la bola
-pausar la acción unos determinados milisegundos.

Estos milisegundos son los que, en teoría, van a controlar la velocidad de movimiento de la bola. Pero luego veremos que no logra cumplir del todo bien este objetivo.

Todo este proceso se repite mientras un atributo boolean tenga valor true.
Este atributo es el que indica si la bola ha de moverse o ha de pararse.

Otro cambio introducido en esta clase es que ahora recibe una referencia del objeto PanelBotones.
Al principio dije que los ActionListener para los botones los íbamos a crear aquí en GestorJuego y con unos getter los haríamos llegar a los botones del tablero.

Pero, para poder controlar de forma óptima que los botones actúen con lógica (por ejemplo no poder incrementar la velocidad si bola no está moviéndose) al final lo mejor es poder activar/desactivar los botones según el estado de la bola.
Y para ello lo más fácil es poder acceder directamente a los botones desde la clase GestorJuego, así que le he dado una referencia al objeto PanelBotones y así puedo cambiar estado de los botones y añadirles los ActionListener sin tener que usar getters.
Código: [Seleccionar]
public class GestorJuego {

private Tablero tablero;
private PanelBotones botones;
private long velocidad;
private boolean mover;
private MovimientoBola movimiento;

public GestorJuego(Tablero tablero, PanelBotones botones) {
this.tablero = tablero;
this.botones = botones;
botones.btIniciar.addActionListener(new AccionIniciar());
botones.btAcelerar.addActionListener(new AccionAcelerar());
botones.btFrenar.addActionListener(new AccionFrenar());
botones.btDetenContinuar.addActionListener(new AccionIniciaDetiene());
botones.btReset.addActionListener(new AccionReset());
velocidad = 25;
mover = false;
}

private class MovimientoBola extends SwingWorker {
@Override
protected Object doInBackground() throws Exception {
while(mover) {
tablero.getBola().mover();
tablero.getBola().comprobarColision();
tablero.repaint();
Thread.sleep(velocidad);
}
return null;
}
}

private class AccionReset implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
mover = false;
tablero.getBola().detener();
tablero.reset();
botones.btIniciar.setEnabled(true);
botones.btAcelerar.setEnabled(false);
botones.btFrenar.setEnabled(false);
botones.btDetenContinuar.setEnabled(false);
botones.btReset.setEnabled(false);
}
}

private class AccionIniciar implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (!mover) {
mover = true;
velocidad = 25;
movimiento = new MovimientoBola();
movimiento.execute();
botones.btIniciar.setEnabled(false);
botones.btAcelerar.setEnabled(true);
botones.btFrenar.setEnabled(true);
botones.btDetenContinuar.setEnabled(true);
botones.btReset.setEnabled(true);
}
}
}

private class AccionAcelerar implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (mover) {
if (velocidad > 5)
velocidad -= 5;
}
}
}

private class AccionFrenar implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (mover)
velocidad += 5;
}
}

private class AccionIniciaDetiene implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (mover) {
mover = false;
botones.btAcelerar.setEnabled(false);
botones.btFrenar.setEnabled(false);
}
else {
mover = true;
movimiento = new MovimientoBola();
movimiento.execute();
botones.btAcelerar.setEnabled(true);
botones.btFrenar.setEnabled(true);
}
}
}

}

La clase PanelBotones apenas cambia, lo único es que en el constructor desactivo todos los botones excepto el botón "Iniciar".
Estos botones luego se irán activando y desactivando según el estado de la bola.
Código: [Seleccionar]
public class PanelBotones extends JPanel {

public JButton btIniciar;
public JButton btAcelerar;
public JButton btFrenar;
public JButton btDetenContinuar;
public JButton btReset;

public PanelBotones(int ancho, int alto) {
btIniciar = new JButton("Iniciar");
btAcelerar = new JButton("Acelerar");
btAcelerar.setEnabled(false);
btFrenar = new JButton("Frenar");
btFrenar.setEnabled(false);
btDetenContinuar = new JButton("Detener/Continuar");
btDetenContinuar.setEnabled(false);
btReset = new JButton("Reset");
btReset.setEnabled(false);
setPreferredSize(new Dimension(ancho, alto));
setLayout(new GridLayout(5, 1));
add(new PanelBoton(btIniciar));
add(new PanelBoton(btAcelerar));
add(new PanelBoton(btFrenar));
add(new PanelBoton(btDetenContinuar));
add(new PanelBoton(btReset));
}

private class PanelBoton extends JPanel {
public PanelBoton(JButton boton) {
add(boton);
}
}

}

Y por último la clase Juego. Solo cambia que aquí ahora ya no le pasamos a los botones los ActionListener como había planeado al principio.
Ahora a la clase GestorJuego le pasamos una referencia a PanelBotones y ya se encargará él de gestionar los botones.
Código: [Seleccionar]
public class Juego extends JFrame {

private Dimension dim;
private Tablero tablero;
private PanelBotones botones;
private GestorJuego gestor;

public Juego() {
dim = Toolkit.getDefaultToolkit().getScreenSize();
int ancho = (int)(dim.getWidth() / 2);
int alto = (int)(dim.getHeight() / 2);

tablero = new Tablero((ancho / 5 * 4), alto);
botones = new PanelBotones(ancho / 5, alto);
gestor = new GestorJuego(tablero, botones);

setLayout(new FlowLayout());
add(tablero);
add(botones);

setTitle("Bola rebota");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});
}
}

Bien, pues si ejecutamos el programa veremos que los botones funcionan correctamente y la bola se desplaza y rebota de forma coherente.

Sin embargo, también veremos que tras algunos rebotes la bola cambia su velocidad sin que hayamos usado los botones "Frenar" y "Acelerar".

¿Por qué?  ???

El problema está en la forma en que calculo las "desviaciones".
La intención de que los valores cambien entre 1 y 5 o bien entre -1 y -5, es para que la bola rebote en distintos angulos.

Si ambas desviaciones adoptan valor 3, se moverá en 135º.
Y si una es 3 y la otra -3, pues se moverá en 45º
Y si una es valor 2 y la otra 5, pues el ángulo será otro, etc...

El problema es que esto no solo afecta al ángulo, si no también a la velocidad.

Si ambas desviaciones adoptan valor 1, también se moverá en 135º, pero mucho más lento que cuando ambas valen 3, y ya ni te cuento si ambas valen 5.

Al final, los milisegundos que usamos para "dormir" al SwingWorker, no es lo único que influye en la velocidad de la Bola.

Ahora mismo no se muy bien como solucionar esto.
Quizás hacer que la velocidad se decida según un valor que una de las dos "desviaciones" tendrá que utilizar obligatoriamente, y según el rebote, esa "obligación" se transmita, o no, a la otra "desviación"

Así, una "desviación" decidirá la velocidad y la otra el ángulo..., no se, no estoy seguro.
Habrá que probarlo, pero hoy ya no, la cama me llama.

Mañana será otro día...  ::)

75
Veamos, he podido avanzar un poco más, pero la verdad no se si voy por buen camino. Da igual, ye veremos donde acaba esto ...  :P

La Bola no nos dicen como crearla. Podría ser una imagen insertada en un JLabel, esto ya lo he hecho otras veces.

Pero voy a intentarlo con un Shape, en este caso, con una Ellipse2D

Así que la clase Bola va a heredar de Ellipse2D.
De momento va a tener 4 atributos: posición X, posición Y, ancho y alto. Estos atributos se calcularán según las dimensiones del Tablero

Al heredar de Ellipse2D, nos obliga a sobre escribir varios métodos heredados.
Uno de ellos, el getBounds2D()... no estoy muy seguro de si lo que le he escrito es válido o no. Ya se verá, de todos modos puede que ni siquiera lo necesitemos.
Lo mismo con el setFrame(), ese incluso lo he dejado vacío.

De momento, esta es la clase Bola. Luego ya se irá viendo si hay que añadirle más cosas, corregirla o tirarla a la papelera xD
Código: [Seleccionar]
public class Bola extends Ellipse2D {

private int posX;
private int posY;
private double ancho;
private double alto;

public Bola(int x, int y, int w, int h) {
posX = x;
posY = y;
ancho = w;
alto = h;
}

@Override
public Rectangle2D getBounds2D() {
Rectangle2D rect = new Rectangle2D.Double(posX, posY, ancho, alto);
return rect.getBounds2D();
}
@Override
public double getX() {
return posX;
}
@Override
public double getY() {
return posY;
}
@Override
public double getWidth() {
return ancho;
}
@Override
public double getHeight() {
return alto;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void setFrame(double x, double y, double w, double h) {

}

}

He modificado la clase Tablero, añadiendo los atributos ancho y alto y también un atributo Bola
Mi intención era inicializar la Bola por separado en la clase GestorJuego (que veremos después) y luego pintarla en Tablero.
Pero no he conseguido/sabido hacer que el tablero la pinte.

Pensé que con el método getGraphics() de Tablero(que es un JPanel) obtendría su contexto gráfico para poder pintar, pero siempre me retornaba valor null  ???

Así que como no he podido pintar desde FUERA del Tablero, voy a tener que pintar desde DENTRO

Así que la Bola se va a inicializar dentro de Tablero y lo que haremos será poder retornarla con un método get para que ya luego la clase GestorJuego pueda interactuar con ella.

Para pintar desde DENTRO, lo que hacemos es sobre escribir el método paint() que Tablero hereda por ser hija de JPanel y ahí pintaremos la Bola.
En teoría, para hacer que se "mueva" lo que haremos será cambiar sus atributos de posicion X e Y  para luego "repintar" el tablero y que la bola se desplace.

Pero antes de "mover" la bola, hay que borrar el tablero, ya que de lo contrario lo que haremos será pintar muchas bolas.
Por eso, antes de pintar la bola, lo que hago es pintar un rectángulo de color blanco que abarca todo el Tablero. Y ya después pintamos la bola.

Le he puesto también un método para "resetear" el tablero, es decir, volver a colocar la bola en su posición inicial.

Código: [Seleccionar]
public class Tablero extends JPanel {

private int ancho;
private int alto;
private Bola bola;

public Tablero(int ancho, int alto) {
this.ancho = ancho;
this.alto = alto;
bola = new Bola(ancho / 2, alto / 2,
ancho / 30, alto / 25);
setPreferredSize(new Dimension(ancho, alto));
setBorder(BorderFactory.createEtchedBorder(0));
setBackground(Color.WHITE);
}

public int getAncho() {
return ancho;
}

public int getAlto() {
return alto;
}

public Bola getBola() {
return bola;
}

public void reset() {
bola = new Bola(ancho / 2, alto / 2,
ancho / 30, alto / 25);
repaint();
}

@Override
public void paint(Graphics g) {
super.paint(g);
        Graphics2D G2D = (Graphics2D)g;
        //Limpiamos panel con un rectangulo blanco
        G2D.setColor(Color.WHITE);
        G2D.fillRect(0, 0, ancho, alto);
        //Ahora pintamos bola
        G2D.setColor(Color.BLUE);
        G2D.fill(bola);
        G2D.draw(bola);
}

}


Veamos la clase GestorJuego.
De momento apenas tiene código, pero va a ser la clase más difícil porque aquí es donde habrá que poner la bola en movimiento, calcular si colisiona, cambiar el ángulo de su dirección....
Y de momento no tengo hecho nada de eso.

Por ahora solo tiene el constructor, que recibe una referencia del Tablero para interactuar con él.
La Bola está dentro del Tablero, así que de hecho podrá interactuar con ambos.

Tiene un método, donde construimos y retornamos un ActionListener para resetear el tablero.
Desde aquí construiremos todos los ActionListeners para las distintas funciones y los retornamos para poder hacerselos llegar a los botones de la clase PanelBotones

Código: [Seleccionar]
public class GestorJuego {

private Tablero tablero;

public GestorJuego(Tablero tablero) {
this.tablero = tablero;
}

public ActionListener getAccionReset() {
return new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
tablero.reset();
}
};
}

}

Por último, en la clase principal Juego. Añado el nuevo atributo GestorJuego y en el constructor vemos cómo le hago llegar al botón Reset la acción para que pueda resetear el tablero.

Código: [Seleccionar]
public class Juego extends JFrame {

private Dimension dim;
private Tablero tablero;
private PanelBotones botones;
private GestorJuego gestor;

public Juego() {
dim = Toolkit.getDefaultToolkit().getScreenSize();
int ancho = (int)(dim.getWidth() / 2);
int alto = (int)(dim.getHeight() / 2);

tablero = new Tablero((ancho / 5 * 4), alto);
botones = new PanelBotones(ancho / 5, alto);
gestor = new GestorJuego(tablero);
botones.btReset.addActionListener(gestor.getAccionReset());

setLayout(new FlowLayout());
add(tablero);
add(botones);

setTitle("Bola rebota");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});
}
}

Si ejecutamos este código, veremos ahora una bolita azul en el centro del tablero.
De momento está inmovil, a ver cuando puedo ponerme a pensar lo difícil, hacer que se mueva y colisione correctamente

Un saludo.

76
Uuff.. interesante, pero complicado... ¿no?
La verdad es que no se muy bien como hacer que la bolita rebote en distintos ángulos y tal..
Moverla, se puede hacer con un SwingWorker o un Thread. Que detecte bordes y rebote en un ángulo u otro, ahí es donde está lo complicado.

Pero bueno, de momento se puede ir creando el tablero y los botones, para tener algo sobre lo que trabajar y experimentar.

No dispongo de mucho tiempo  :( pero he podido hacer la interfaz básica.
Los botones no están bonitos, pero bueno, ocuparnos de su estética sería lo último

Dejo esto por aquí, que como digo ahora mismo no tiene ninguna funcionalidad, pero es una interfaz sobre la que ir trabajando.
Puedes usarla o si tú ya has creado otra, compártela por aquí también y así vamos viendo distintas soluciones.

Clase Tablero
Código: [Seleccionar]
public class Tablero extends JPanel {

public Tablero(int ancho, int alto) {
setPreferredSize(new Dimension(ancho, alto));
setBorder(BorderFactory.createEtchedBorder(0));
setBackground(Color.WHITE);
}

}

Clase PanelBotones
Código: [Seleccionar]
public class PanelBotones extends JPanel {

public JButton btIniciar;
public JButton btAcelerar;
public JButton btFrenar;
public JButton btDetenContinuar;
public JButton btReset;

public PanelBotones(int ancho, int alto) {
btIniciar = new JButton("Iniciar");
btAcelerar = new JButton("Acelerar");
btFrenar = new JButton("Frenar");
btDetenContinuar = new JButton("Detener/Continuar");
btReset = new JButton("Reset");
setPreferredSize(new Dimension(ancho, alto));
setLayout(new GridLayout(5, 1));
add(new PanelBoton(btIniciar));
add(new PanelBoton(btAcelerar));
add(new PanelBoton(btFrenar));
add(new PanelBoton(btDetenContinuar));
add(new PanelBoton(btReset));
}

private class PanelBoton extends JPanel {
public PanelBoton(JButton boton) {
add(boton);
}
}

}

Y clase Juego. Aquí es donde obtenemos las dimensiones de la pantalla para así decirle a los otros dos paneles cuáles han de ser sus dimensiones.
Código: [Seleccionar]
public class Juego extends JFrame {

private Dimension dim;
private Tablero tablero;
private PanelBotones botones;

public Juego() {
dim = Toolkit.getDefaultToolkit().getScreenSize();
int ancho = (int)(dim.getWidth() / 2);
int alto = (int)(dim.getHeight() / 2);

tablero = new Tablero((ancho / 5 * 4), alto);
botones = new PanelBotones(ancho / 5, alto);
setLayout(new FlowLayout());
add(tablero);
add(botones);

setTitle("Bola rebota");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Juego();
}
});
}
}


De momento no he podido hacer más.
Iré comentando por aquí cuando pueda seguir.

Un saludo.

77
De todo un poco... / Re: Gramática y Expresiones regulares
« en: 07 de Abril 2023, 10:31 »
Vaya, todo esto de los lenguajes y autómatas me supera  :(
Espero que pase alguien por aquí que sí pueda ayudarte.

Un saludo y bienvenido.

78
Muy poca. La mayoría de las veces puede incorporar algún tipo de malware.

Microsoft Office lo puedes usar de forma gratuita y legal, desde su versión online

Por otra parte, tienes alternativas de software libre que pueden cubrir perfectamente la mayoría de necesidades ofimáticas.
Por ejemplo, Libre Office. Llevo años usándolo y sin echar en falta la suite de Microsoft.

79
Hola.
El proceso (suponiendo que tengas Windows 10/11) sería entrar en "Sistema" y:
- Paso 1 --> Elegir "Configuración Avanzada del Sistema"
- Paso 2 -->Clickar botón "Variables de Entorno"
- Paso 3 --> Ahí, en el cuadro "Variables del Sistema" encontraremos la variable "Path".
La seleccionamos y pulsamos botón "Editar"
- Paso 4 --> Con el botón "Nuevo" podemos escribir directamente la ruta donde tengamos Java instalado.
Pero quizás sea más fácil usar el botón "Examinar" con el que se nos abrirá un explorador para buscar la carpeta Java y así evitamos errores por escribir mal la ruta.


80
Comunidad / Re: Presentación
« en: 19 de Marzo 2023, 22:30 »
Bienvenido Raúl.

Espero que te encuentres a gusto aquí y puedas aprender y enseñar a partes iguales.

Un saludo.

Páginas: 1 2 3 [4] 5 6 7 8 9 ... 50

Sobre la educación, sólo puedo decir que es el tema más importante en el que nosotros, como pueblo, debemos involucrarnos.

Abraham Lincoln (1808-1865) Presidente estadounidense.

aprenderaprogramar.com: Desde 2006 comprometidos con la didáctica y divulgación de la programación

Preguntas y respuestas

¿Cómo establecer o cambiar la imagen asociada (avatar) de usuario?
  1. Inicia sesión con tu nombre de usuario y contraseña.
  2. Pulsa en perfil --> perfil del foro
  3. Elige la imagen personalizada que quieras usar. Puedes escogerla de una galería de imágenes o subirla desde tu ordenador.
  4. En la parte final de la página pulsa el botón "cambiar perfil".