Saltar al contenido
proyecto-cartas.java · devschool

Proyecto en Java: una baraja de cartas

Lección 39 de 39 · 15 min de lectura · Actualizado el

En esta lección
  1. Qué vamos a construir
  2. Plan: qué piezas necesitamos
  3. Paso 1: los palos
  4. Paso 2: los rangos
  5. Paso 3: la carta como record
  6. Paso 4: la baraja
  7. Paso 5: las jugadas
  8. Paso 6: la mano y su evaluación
  9. Paso 7: jugar una partida
  10. Código final completo
  11. Probar la evaluación con manos conocidas
  12. Ideas para ampliar el proyecto
  13. Errores frecuentes
  14. Resumen

Es hora de juntar lo aprendido en un programa completo. En este proyecto construirás una baraja española de 40 cartas: la crearás con enums y records, la barajarás, repartirás manos de cuatro cartas a varios jugadores, evaluarás qué jugada tiene cada uno (pareja, trío, póker…) y decidirás quién gana. Todo con Java 21, sin librerías externas y en un solo archivo.

Qué vamos a construir

Cada jugador recibe cuatro cartas. La mano se puntúa así, de menor a mayor:

JugadaQué es
Sin paresTodas las cartas distintas
ParejaDos cartas del mismo número
Doble parejaDos parejas distintas
TríoTres cartas del mismo número
PókerCuatro cartas del mismo número

Si dos jugadores empatan en jugada, gana el que tenga el número más alto en el grupo más repetido. Para el orden de los números usaremos el del propio valor: el as es el más bajo (1) y el rey, el más alto (12).

La baraja española tiene cuatro palos (oros, copas, espadas y bastos) y diez rangos por palo: del as al siete, la sota, el caballo y el rey. Los ochos y los nueves no existen.

Plan: qué piezas necesitamos

Antes de escribir código, decide qué es cada cosa:

  • Palo y Rango: valores fijos y limitados, así que son enum.
  • Carta: guarda un rango y un palo y no cambia nunca, por lo que es un record.
  • Baraja: tiene una lista de cartas y operaciones (barajar, robar, repartir), así que es una class.
  • Jugada: las categorías posibles, otro enum.
  • Mano: un conjunto de cartas que sabe decir qué jugada es y compararse con otra, un record que implementa Comparable.

Crea el archivo Cartas.java y ve añadiendo las piezas dentro de la clase Cartas, en este orden. Al final tendrás el código completo.

Paso 1: los palos

enum Palo {
    OROS("oros"), COPAS("copas"), ESPADAS("espadas"), BASTOS("bastos");

    private final String nombre;

    Palo(String nombre) { this.nombre = nombre; }

    String nombre() { return nombre; }
}

Cada constante lleva su texto en minúsculas para mostrarlo bonito (“oros” en vez de OROS). Un enum puede tener atributos y constructor, como viste en la lección de enums.

Paso 2: los rangos

enum Rango {
    AS("as", 1), DOS("2", 2), TRES("3", 3), CUATRO("4", 4), CINCO("5", 5),
    SEIS("6", 6), SIETE("7", 7), SOTA("sota", 10), CABALLO("caballo", 11), REY("rey", 12);

    private final String etiqueta;
    private final int numero;

    Rango(String etiqueta, int numero) {
        this.etiqueta = etiqueta;
        this.numero = numero;
    }

    String etiqueta() { return etiqueta; }

    int numero() { return numero; }
}

Hay dos datos por rango: la etiqueta para mostrar y el número con el que se compara. Los números 10, 11 y 12 son los de las figuras en la baraja española. Todo en un enum: añadir un rango nuevo es añadir una línea.

Paso 3: la carta como record

record Carta(Rango rango, Palo palo) {
    @Override
    public String toString() {
        return rango.etiqueta() + " de " + palo.nombre();
    }
}

El equals y el hashCode vienen gratis: dos cartas con el mismo rango y palo son iguales. Solo sobrescribimos toString para obtener “rey de oros” en lugar de Carta[rango=REY, palo=OROS].

Paso 4: la baraja

static class Baraja {
    private final List<Carta> cartas = new ArrayList<>();

    Baraja() {
        for (Palo palo : Palo.values()) {
            for (Rango rango : Rango.values()) {
                cartas.add(new Carta(rango, palo));
            }
        }
    }

    int restantes() { return cartas.size(); }

    void barajar(Random azar) { Collections.shuffle(cartas, azar); }

    Carta robar() {
        if (cartas.isEmpty()) {
            throw new IllegalStateException("La baraja está vacía");
        }
        return cartas.remove(cartas.size() - 1);
    }

    List<Carta> repartir(int cuantas) {
        if (cuantas > cartas.size()) {
            throw new IllegalArgumentException("Quedan " + cartas.size() + " cartas y piden " + cuantas);
        }
        List<Carta> mano = new ArrayList<>();
        for (int i = 0; i < cuantas; i++) {
            mano.add(robar());
        }
        return mano;
    }
}

Puntos clave:

  • El constructor recorre los 4 palos y los 10 rangos con values() y crea 40 cartas.
  • Collections.shuffle mezcla la lista al azar. Le pasamos un objeto Random en lugar de dejarlo implícito. Con new Random(7) (una semilla fija) el barajado es siempre el mismo, útil para probar y depurar; con new Random() cada partida es distinta.
  • robar() quita la última carta de la lista, que es lo más eficiente en un ArrayList y equivale a coger la de arriba del mazo. Si no quedan cartas, lanza una excepción en vez de fallar de forma extraña.
  • repartir roba varias cartas y las devuelve en una lista.

Prueba lo que llevas con un main provisional:

public static void main(String[] args) {
    Baraja baraja = new Baraja();
    System.out.println(baraja.restantes());   // 40
    baraja.barajar(new Random(7));
    System.out.println(baraja.robar());       // 7 de bastos
    System.out.println(baraja.restantes());   // 39
}

Recuerda el import java.util.*; (o los imports uno a uno, como en el código final). La carta que sale con la semilla 7 es siempre “7 de bastos”.

Paso 5: las jugadas

enum Jugada {
    NADA("Sin pares"), PAREJA("Pareja"), DOBLE_PAREJA("Doble pareja"), TRIO("Trío"), POKER("Póker");

    private final String texto;

    Jugada(String texto) { this.texto = texto; }

    String texto() { return texto; }
}

El orden de declaración importa: compareTo de un enum compara por posición, así que POKER es mayor que TRIO, y este mayor que NADA. No hace falta una tabla de puntuaciones.

Paso 6: la mano y su evaluación

La idea para evaluar es contar cuántas veces aparece cada rango. Con las repeticiones ordenadas de mayor a menor, la jugada se deduce sola: [4] es póker, [3, 1] trío, [2, 2] doble pareja, [2, 1, 1] pareja y [1, 1, 1, 1] nada.

record Mano(List<Carta> cartas) implements Comparable<Mano> {
    Mano {
        cartas = List.copyOf(cartas);
    }

    private Map<Rango, Integer> contar() {
        Map<Rango, Integer> veces = new EnumMap<>(Rango.class);
        for (Carta c : cartas) {
            veces.merge(c.rango(), 1, Integer::sum);
        }
        return veces;
    }

    Jugada jugada() {
        List<Integer> repeticiones = new ArrayList<>(contar().values());
        repeticiones.sort(Comparator.reverseOrder());
        return switch (repeticiones.get(0)) {
            case 4 -> Jugada.POKER;
            case 3 -> Jugada.TRIO;
            case 2 -> repeticiones.get(1) == 2 ? Jugada.DOBLE_PAREJA : Jugada.PAREJA;
            default -> Jugada.NADA;
        };
    }
    // ... sigue con el desempate
}

Novedades de este paso:

  • El constructor compacto hace una copia defensiva con List.copyOf (mira records): nadie podrá cambiar la mano desde fuera.
  • EnumMap es un mapa pensado para claves de tipo enum: rápido y ordenado según la declaración. merge(c.rango(), 1, Integer::sum) suma 1 al contador de ese rango, empezando en 1 si no existía.
  • El switch con flechas devuelve la jugada. En el caso 2 miramos la segunda repetición: si también es 2, hay dos parejas.

Cuidado: jugada() supone manos de al menos dos cartas. Con menos, repeticiones.get(1) fallaría. Aquí siempre repartimos cuatro; si cambias eso, ten el caso en cuenta.

El desempate

Para comparar dos manos de la misma jugada, ordenamos los rangos de cada mano por cuántas veces aparecen (más primero) y, en caso de igualdad, por su número (más alto primero). Luego comparamos las dos listas de izquierda a derecha:

    private List<Rango> desempate() {
        Map<Rango, Integer> veces = contar();
        List<Rango> rangos = new ArrayList<>(veces.keySet());
        rangos.sort(Comparator
                .comparing((Rango r) -> veces.get(r)).reversed()
                .thenComparing(Comparator.comparingInt(Rango::numero).reversed()));
        return rangos;
    }

    @Override
    public int compareTo(Mano otra) {
        int porJugada = jugada().compareTo(otra.jugada());
        if (porJugada != 0) {
            return porJugada;
        }
        List<Rango> a = desempate();
        List<Rango> b = otra.desempate();
        for (int i = 0; i < Math.min(a.size(), b.size()); i++) {
            int c = Integer.compare(a.get(i).numero(), b.get(i).numero());
            if (c != 0) {
                return c;
            }
        }
        return 0;
    }

Por ejemplo, entre una pareja de reyes y una pareja de seises, ambas dan [REY, ...] y [SEIS, ...] en primer lugar, y gana la de reyes. La lógica de comparadores está explicada en Comparable y Comparator.

Paso 7: jugar una partida

El main crea la baraja, la baraja, reparte cuatro cartas a cada jugador y decide quién gana:

public static void main(String[] args) {
    Baraja baraja = new Baraja();
    baraja.barajar(new Random(7));

    String[] nombres = {"Ana", "Luis", "Marta"};
    Map<String, Mano> manos = new LinkedHashMap<>();
    for (String nombre : nombres) {
        manos.put(nombre, new Mano(baraja.repartir(4)));
    }

    String ganador = null;
    Mano mejor = null;
    for (Map.Entry<String, Mano> e : manos.entrySet()) {
        System.out.println(e.getKey() + ": " + e.getValue() + " -> " + e.getValue().jugada().texto());
        if (mejor == null || e.getValue().compareTo(mejor) > 0) {
            mejor = e.getValue();
            ganador = e.getKey();
        }
    }
    System.out.println("Gana " + ganador + " con " + mejor.jugada().texto());
    System.out.println("Quedan " + baraja.restantes() + " cartas en la baraja");
}

LinkedHashMap conserva el orden de inserción, de modo que los jugadores salen en el mismo orden en que se repartió.

Código final completo

Guarda todo en Cartas.java, compila con javac Cartas.java y ejecuta con java Cartas:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

public class Cartas {

    enum Palo {
        OROS("oros"), COPAS("copas"), ESPADAS("espadas"), BASTOS("bastos");

        private final String nombre;

        Palo(String nombre) { this.nombre = nombre; }

        String nombre() { return nombre; }
    }

    enum Rango {
        AS("as", 1), DOS("2", 2), TRES("3", 3), CUATRO("4", 4), CINCO("5", 5),
        SEIS("6", 6), SIETE("7", 7), SOTA("sota", 10), CABALLO("caballo", 11), REY("rey", 12);

        private final String etiqueta;
        private final int numero;

        Rango(String etiqueta, int numero) {
            this.etiqueta = etiqueta;
            this.numero = numero;
        }

        String etiqueta() { return etiqueta; }

        int numero() { return numero; }
    }

    record Carta(Rango rango, Palo palo) {
        @Override
        public String toString() {
            return rango.etiqueta() + " de " + palo.nombre();
        }
    }

    static class Baraja {
        private final List<Carta> cartas = new ArrayList<>();

        Baraja() {
            for (Palo palo : Palo.values()) {
                for (Rango rango : Rango.values()) {
                    cartas.add(new Carta(rango, palo));
                }
            }
        }

        int restantes() { return cartas.size(); }

        void barajar(Random azar) { Collections.shuffle(cartas, azar); }

        Carta robar() {
            if (cartas.isEmpty()) {
                throw new IllegalStateException("La baraja está vacía");
            }
            return cartas.remove(cartas.size() - 1);
        }

        List<Carta> repartir(int cuantas) {
            if (cuantas > cartas.size()) {
                throw new IllegalArgumentException("Quedan " + cartas.size() + " cartas y piden " + cuantas);
            }
            List<Carta> mano = new ArrayList<>();
            for (int i = 0; i < cuantas; i++) {
                mano.add(robar());
            }
            return mano;
        }
    }

    enum Jugada {
        NADA("Sin pares"), PAREJA("Pareja"), DOBLE_PAREJA("Doble pareja"), TRIO("Trío"), POKER("Póker");

        private final String texto;

        Jugada(String texto) { this.texto = texto; }

        String texto() { return texto; }
    }

    record Mano(List<Carta> cartas) implements Comparable<Mano> {
        Mano {
            cartas = List.copyOf(cartas);
        }

        private Map<Rango, Integer> contar() {
            Map<Rango, Integer> veces = new EnumMap<>(Rango.class);
            for (Carta c : cartas) {
                veces.merge(c.rango(), 1, Integer::sum);
            }
            return veces;
        }

        Jugada jugada() {
            List<Integer> repeticiones = new ArrayList<>(contar().values());
            repeticiones.sort(Comparator.reverseOrder());
            return switch (repeticiones.get(0)) {
                case 4 -> Jugada.POKER;
                case 3 -> Jugada.TRIO;
                case 2 -> repeticiones.get(1) == 2 ? Jugada.DOBLE_PAREJA : Jugada.PAREJA;
                default -> Jugada.NADA;
            };
        }

        private List<Rango> desempate() {
            Map<Rango, Integer> veces = contar();
            List<Rango> rangos = new ArrayList<>(veces.keySet());
            rangos.sort(Comparator
                    .comparing((Rango r) -> veces.get(r)).reversed()
                    .thenComparing(Comparator.comparingInt(Rango::numero).reversed()));
            return rangos;
        }

        @Override
        public int compareTo(Mano otra) {
            int porJugada = jugada().compareTo(otra.jugada());
            if (porJugada != 0) {
                return porJugada;
            }
            List<Rango> a = desempate();
            List<Rango> b = otra.desempate();
            for (int i = 0; i < Math.min(a.size(), b.size()); i++) {
                int c = Integer.compare(a.get(i).numero(), b.get(i).numero());
                if (c != 0) {
                    return c;
                }
            }
            return 0;
        }

        @Override
        public String toString() {
            return cartas.toString();
        }
    }

    public static void main(String[] args) {
        Baraja baraja = new Baraja();
        baraja.barajar(new Random(7));

        String[] nombres = {"Ana", "Luis", "Marta"};
        Map<String, Mano> manos = new LinkedHashMap<>();
        for (String nombre : nombres) {
            manos.put(nombre, new Mano(baraja.repartir(4)));
        }

        String ganador = null;
        Mano mejor = null;
        for (Map.Entry<String, Mano> e : manos.entrySet()) {
            System.out.println(e.getKey() + ": " + e.getValue() + " -> " + e.getValue().jugada().texto());
            if (mejor == null || e.getValue().compareTo(mejor) > 0) {
                mejor = e.getValue();
                ganador = e.getKey();
            }
        }
        System.out.println("Gana " + ganador + " con " + mejor.jugada().texto());
        System.out.println("Quedan " + baraja.restantes() + " cartas en la baraja");
    }
}

Con la semilla 7, la salida es siempre esta:

Ana: [7 de bastos, caballo de bastos, 2 de oros, 3 de oros] -> Sin pares
Luis: [caballo de espadas, 5 de bastos, caballo de copas, rey de espadas] -> Pareja
Marta: [3 de espadas, 4 de espadas, as de oros, 3 de copas] -> Pareja
Gana Luis con Pareja
Quedan 28 cartas en la baraja

Luis y Marta empatan en jugada (pareja). Gana Luis porque su pareja es de caballos (11) y la de Marta, de treses.

Para ver partidas distintas, cambia new Random(7) por new Random().

Probar la evaluación con manos conocidas

Barajar al azar rara vez da un póker, así que para comprobar que jugada() funciona crea manos a mano. Añade un método auxiliar y un main de pruebas:

static Mano mano(Rango... rangos) {
    List<Carta> cartas = new ArrayList<>();
    Palo[] palos = Palo.values();
    for (int i = 0; i < rangos.length; i++) {
        cartas.add(new Carta(rangos[i], palos[i % palos.length]));
    }
    return new Mano(cartas);
}

// en un main de pruebas:
Mano poker = mano(Rango.REY, Rango.REY, Rango.REY, Rango.REY);
Mano dobles = mano(Rango.SEIS, Rango.SEIS, Rango.AS, Rango.AS);
System.out.println(poker.jugada());            // POKER
System.out.println(dobles.jugada());           // DOBLE_PAREJA
System.out.println(dobles.compareTo(poker));   // -2

El -2 sale de comparar DOBLE_PAREJA (posición 2) con POKER (posición 4). Fíjate en que aquí las cuatro cartas repetidas llevan palos distintos y eso no influye en la jugada.

Ideas para ampliar el proyecto

  • Baraja francesa de 52 cartas. Cambia Palo por picas, corazones, diamantes y tréboles, y Rango por 2-10, J, Q, K y A. Con cinco cartas por mano puedes añadir escalera, color, full…
  • Siete y media. Cada carta vale su número (las figuras valen 0,5) y gana quien más se acerque a 7,5 sin pasarse. Añade un atributo puntos a Rango.
  • Interfaz por consola. Pide el número de jugadores con Scanner (entrada por teclado) y juega varias rondas.
  • Guardar las partidas en un fichero con ficheros.
  • Pruebas automáticas de jugada() con JUnit.
  • Mazo que se rellena: cuando se acaban las cartas, vuelve a crear y barajar una baraja.

Errores frecuentes

  • Barajar sin pasar el Random o con la misma semilla siempre. Con new Random(7) sale siempre lo mismo; para jugar de verdad usa new Random() o Collections.shuffle(cartas) a secas.
  • Robar de una baraja vacía. Controla el número de cartas, como hace robar(), y lanza una excepción clara.
  • Modificar la lista mientras la recorres. Si quitas cartas dentro de un for sobre la misma lista, obtendrás ConcurrentModificationException. Usa remove fuera del bucle o un Iterator.
  • Olvidar las manos de otro tamaño. El método jugada() espera al menos dos cartas.
  • Comparar enums con equals en vez de == o compareTo. Funciona, pero == es más claro y compareTo aprovecha el orden de declaración.
  • Cambiar el orden de las constantes de Jugada. Se romperá la comparación: el orden de declaración es la puntuación.
  • Mutar la lista de una mano. Por eso Mano hace List.copyOf en el constructor compacto.

Resumen

PiezaTipoPara qué sirve
Palo, Rango, JugadaenumValores fijos con datos propios
CartarecordDato inmutable con equals y toString
BarajaclassEstado que cambia: barajar, robar, repartir
Collections.shuffleMétodoMezcla la lista (con o sin semilla)
Manorecord con ComparableEvalúa jugada y compara manos
EnumMapMapaContar rangos con claves enum
LinkedHashMapMapaJugadores en orden de reparto

Con este proyecto cierras el curso de Java: ya sabes escribir clases, usar colecciones y streams, tratar errores y organizar un programa completo. El siguiente paso natural es probar tus ideas con JUnit o dar el salto a otro lenguaje con el curso de Python.

Pon a prueba lo que has aprendido

[Java] En el proyecto de cartas barajas dos barajas nuevas con Collections.shuffle(cartas, new Random(7)). ¿Qué ocurre?

[Java] Una mano tiene REY, REY, REY y AS. Con el método de contar repeticiones del proyecto, ¿qué Jugada se obtiene?
Map<Rango, Integer> veces = new EnumMap<>(Rango.class);
for (Carta c : cartas) {
    veces.merge(c.rango(), 1, Integer::sum);
}
// contenido final: REY=3, AS=1

[Java] ¿Por qué en el enum Jugada las constantes se declaran en el orden NADA, PAREJA, DOBLE_PAREJA, TRIO, POKER?

[Java] ¿Qué contiene el mapa veces después de ejecutar este código con las cartas REY, REY y AS?
Map<Rango, Integer> veces = new EnumMap<>(Rango.class);
for (Rango r : List.of(Rango.REY, Rango.REY, Rango.AS)) {
    veces.merge(r, 1, Integer::sum);
}
System.out.println(veces);

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.