Proyecto en Java: una baraja de cartas
En esta lección
- Qué vamos a construir
- Plan: qué piezas necesitamos
- Paso 1: los palos
- Paso 2: los rangos
- Paso 3: la carta como record
- Paso 4: la baraja
- Paso 5: las jugadas
- Paso 6: la mano y su evaluación
- Paso 7: jugar una partida
- Código final completo
- Probar la evaluación con manos conocidas
- Ideas para ampliar el proyecto
- Errores frecuentes
- 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:
| Jugada | Qué es |
|---|---|
| Sin pares | Todas las cartas distintas |
| Pareja | Dos cartas del mismo número |
| Doble pareja | Dos parejas distintas |
| Trío | Tres cartas del mismo número |
| Póker | Cuatro 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:
PaloyRango: valores fijos y limitados, así que sonenum.Carta: guarda un rango y un palo y no cambia nunca, por lo que es unrecord.Baraja: tiene una lista de cartas y operaciones (barajar, robar, repartir), así que es unaclass.Jugada: las categorías posibles, otroenum.Mano: un conjunto de cartas que sabe decir qué jugada es y compararse con otra, unrecordque implementaComparable.
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.shufflemezcla la lista al azar. Le pasamos un objetoRandomen lugar de dejarlo implícito. Connew Random(7)(una semilla fija) el barajado es siempre el mismo, útil para probar y depurar; connew Random()cada partida es distinta.robar()quita la última carta de la lista, que es lo más eficiente en unArrayListy equivale a coger la de arriba del mazo. Si no quedan cartas, lanza una excepción en vez de fallar de forma extraña.repartirroba 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. EnumMapes 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
switchcon flechas devuelve la jugada. En el caso2miramos 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
Palopor picas, corazones, diamantes y tréboles, yRangopor 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
puntosaRango. - 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
Randomo con la misma semilla siempre. Connew Random(7)sale siempre lo mismo; para jugar de verdad usanew Random()oCollections.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
forsobre la misma lista, obtendrásConcurrentModificationException. Usaremovefuera del bucle o unIterator. - Olvidar las manos de otro tamaño. El método
jugada()espera al menos dos cartas. - Comparar enums con
equalsen vez de==ocompareTo. Funciona, pero==es más claro ycompareToaprovecha 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
ManohaceList.copyOfen el constructor compacto.
Resumen
| Pieza | Tipo | Para qué sirve |
|---|---|---|
Palo, Rango, Jugada | enum | Valores fijos con datos propios |
Carta | record | Dato inmutable con equals y toString |
Baraja | class | Estado que cambia: barajar, robar, repartir |
Collections.shuffle | Método | Mezcla la lista (con o sin semilla) |
Mano | record con Comparable | Evalúa jugada y compara manos |
EnumMap | Mapa | Contar rangos con claves enum |
LinkedHashMap | Mapa | Jugadores 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
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.