Hoy veremos cómo programar el juego de 3 en línea en Java. Este juego también es conocido como Tres en raya, ceros y cruces, triqui, gato, juego del gato, Gatos o gatitos, dependiendo del país.
El juego consiste en un tablero de 3 x 3 en donde se coloca un círculo o una cruz. El juego se desarrolla por turnos y cada jugador elige ser Cruz o Círculo. Gana el jugador que pueda conectar 3 símbolos formando una línea recta.
En el juego que vamos a programar en Java vamos a tener la opción de jugar contra un amigo (otro ser humano), contra el CPU (con una pequeña inteligencia artificial) o enfrentar al CPU contra sí mismo.
Vamos a dividir todo el comportamiento del juego en métodos, de modo que cada función se encargue de una cosa y podamos repartir el trabajo de todo el juego.
El juego de Gato se desarrolla en una matriz en donde vamos a ir colocando las piezas del jugador, ya sea la O
o la X
. Básicamente vamos a ir colocando las piezas y comprobando si una de ellas forma una línea recta de más de 3 piezas seguidas.
Para saber si hay empate vamos a comprobar si todo el tablero tiene piezas. En cuanto al CPU vamos a usar un algoritmo muy parecido al de Conecta 4 para ver en qué lugar colocar la pieza.
Necesitamos el modo de juego para saber cómo jugar Tres en raya programado con Java. En este caso ya expliqué los 3 modos, así que lo solicitamos al inicio:
public static void main(String[] args) {
int modo;
String menu = "1. Humano contra humano\n2. Humano contra CPU (El CPU juega como " + JUGADOR_CPU_O + ")\n3. CPU contra CPU\nElige: ";
modo = solicitarNumeroValido(menu, 1, 3);
iniciarJuego(modo);
sc.close();
}
Fíjate en que estoy usando un Scanner y lo estoy cerrando al final de todo. Por cierto, estoy utilizando una función ya presentada en mi blog, misma que solicita un número válido:
public static int solicitarNumeroValido(String mensaje, int minimo, int maximo) {
int numero;
while (true) {
System.out.println(mensaje);
if (sc.hasNextInt()) {
numero = sc.nextInt();
if (numero >= minimo && numero <= maximo) {
return numero;
} else {
System.out.println("Número fuera de rango. Intente de nuevo");
}
} else {
sc.next();
}
}
}
Ahora veamos la verdadera función que inicia el juego, misma que es la que lleva todo el loop principal:
static void iniciarJuego(int modo) {
if (modo != JUGADOR_JUGADOR && modo != JUGADOR_CPU && modo != CPU_CPU) {
System.out.print("Modo de juego no permitido");
return;
}
// Para que salgan cosas aleatorias
// Iniciar tablero de juego
char[][] tablero = new char[FILAS][COLUMNAS];
// Y limpiarlo
limpiarTablero(tablero);
// Elegir jugador que inicia al azar
char jugadorActual = jugadorAleatorio();
System.out.printf("El jugador que inicia es: %c\n", jugadorActual);
int x = 0, y = 0;
// Y allá vamos
int[] coordenadas = new int[2];
while (true) {
imprimirTablero(tablero);
if (modo == JUGADOR_JUGADOR || (modo == JUGADOR_CPU && jugadorActual == JUGADOR_X)) {
System.out.printf("Jugador %c. Ingresa coordenadas (x,y) para colocar la pieza\n", jugadorActual);
do {
x = solicitarNumeroValido("Ingresa X: ", 1, COLUMNAS);
y = solicitarNumeroValido("Ingresa Y: ", 1, FILAS);
if (!coordenadasVacias(y - 1, x - 1, tablero)) {
System.out.println("Coordenadas ocupadas. Intenta de nuevo");
}
} while (!coordenadasVacias(y - 1, x - 1, tablero));
// Al usuario se le solicitan números comenzando a contar en 1, pero en los arreglos comenzamos desde el 0
// así que necesitamos restar uno en ambas variables
x--;
y--;
} else if (modo == CPU_CPU || jugadorActual == JUGADOR_CPU_O) {
// Si es modo CPU contra CPU o es el turno del CPU, dejamos que las coordenadas las elija
// el programa
coordenadas = elegirCoordenadasCpu(jugadorActual, tablero);
x = coordenadas[0];
y = coordenadas[1];
}
// Sin importar cuál modo haya sido, colocamos la pieza según las coordenadas elegidas
colocarPieza(y, x, jugadorActual, tablero);
// Puede que después de colocar la pieza el jugador gane o haya un empate, así que comprobamos
if (comprobarSiGana(jugadorActual, tablero)) {
imprimirTablero(tablero);
System.out.printf("El jugador %c gana\n", jugadorActual);
return;
} else if (empate(tablero)) {
imprimirTablero(tablero);
System.out.print("Empate");
return;
}
// Si no, es turno del otro jugador
jugadorActual = oponenteDe(jugadorActual);
}
}
Como te puedes dar cuenta, el tablero de juego es un array de tipo char
. Esto es porque lo he portado desde C en donde no es tan fácil manejar las cadenas. Y así de simple es todo este juego de Tres en línea con Java. Ahora profundizaremos en las funciones ayudantes.
Tenemos la función que imprime el tablero de juego para informarle al usuario de la situación:
// Imprime el tablero de juego
static void imprimirTablero(char[][] tablero) {
System.out.print("\n");
int y;
int x;
// Imprimir encabezado
System.out.print("| ");
for (x = 0; x < COLUMNAS; x++) {
System.out.printf("|%d", x + 1);
}
System.out.print("|\n");
for (y = 0; y < FILAS; y++) {
System.out.printf("|%d", y + 1);
for (x = 0; x < COLUMNAS; x++) {
System.out.printf("|%c", tablero[y][x]);
}
System.out.print("|\n");
}
}
Simplemente aplicamos un formato y vamos imprimiendo todo con System.out.printf
. También tenemos otra función que nos dice si el tablero está vacío en determinadas coordenadas (así sabemos si el usuario puede colocar la pieza en esa posición):
// Indica si el tablero está vacío en las coordenadas indicadas
static boolean coordenadasVacias(int y, int x, char[][] tablero) {
return tablero[y][x] == ESPACIO_VACIO;
}
Ya te lo dije antes: este juego de Gato en Java tiene un modo para jugar contra el CPU, así que debemos tener una función que elija la mejor posición para colocar la pieza. Para ello hacemos una simulación simple sin meternos en árboles de probabilidad.
Antes que nada, veamos la función que clona la matriz para simular y no afectar al tablero original:
// Clona la matriz. Útil para las simulaciones que se hacen, así no se modifica el tablero original
static char[][] clonarMatriz(char[][] tableroOriginal) {
char[][] copia = new char[FILAS][COLUMNAS];
for (int y = 0; y < FILAS; y++) {
for (int x = 0; x < COLUMNAS; x++) {
copia[y][x] = tableroOriginal[y][x];
}
}
return copia;
}
Ahora vemos la función que se basa en que el CPU elige en el siguiente orden:
Invocando a distintos métodos:
// Hace que el CPU elija unas coordenadas para ganar
static int[] elegirCoordenadasCpu(char jugador, char[][] tablero) {
hablar("Estoy pensando...", jugador);
/*
El orden en el que el CPU infiere las coordenadas que toma es:
1. Ganar si se puede
2. Hacer perder al oponente si está a punto de ganar
3. Tomar el mejor movimiento del oponente (en donde obtiene el mayor puntaje)
4. Tomar mi mejor movimiento (en donde obtengo mayor puntaje)
5. Elegir la de la esquina superior izquierda (0,0)
6. Coordenadas aleatorias
*/ int y, x, conteoJugador, conteoOponente;
int yOponente, xOponente;
int[] coordenadas = new int[2];
char oponente = oponenteDe(jugador);
// 1
coordenadas = coordenadasParaGanar(jugador, tablero);
x = coordenadas[0];
y = coordenadas[1];
if (y != -1 && x != -1) {
hablar("Ganar", jugador);
return new int[]{x, y};
}
// 2
coordenadas = coordenadasParaGanar(oponente, tablero);
x = coordenadas[0];
y = coordenadas[1];
if (y != -1 && x != -1) {
hablar("Tomar victoria de oponente", jugador);
return new int[]{x, y};
}
// 3
int[] coordenadasJugador = coordenadasParaMayorPuntaje(jugador, tablero);
int[] coordenadasOponente = coordenadasParaMayorPuntaje(oponente, tablero);
conteoJugador = coordenadasJugador[0];
x = coordenadasJugador[1];
y = coordenadasJugador[2];
conteoOponente = coordenadasOponente[0];
xOponente = coordenadasOponente[1];
yOponente = coordenadasOponente[2];
if (conteoOponente > conteoJugador) {
hablar("Tomar puntaje mayor del oponente", jugador);
return new int[]{xOponente, yOponente};
} else if (conteoJugador > 0) {
hablar("Tomar mi mayor puntaje", jugador);
return new int[]{x, y};
}
// 4
if (coordenadasVacias(0, 0, tablero)) {
hablar("Tomar columna superior izquierda", jugador);
return new int[]{0, 0};
}
// 5
hablar("Coordenadas aleatorias", jugador);
coordenadas = obtenerCoordenadasAleatorias(jugador, tablero);
return coordenadas;
}
Mismas que vemos a continuación, poco a poco. Por ejemplo, la función que indica las coordenadas para ganar es la siguiente:
// Devuelve las coordenadas en las que se puede ganar, o -1 y -1 si no se puede ganar
static int[] coordenadasParaGanar(char jugador, char[][] tableroOriginal) {
int y, x;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
char[][] copiaTablero = clonarMatriz(tableroOriginal);
if (coordenadasVacias(y, x, copiaTablero)) {
colocarPieza(y, x, jugador, copiaTablero);
if (comprobarSiGana(jugador, copiaTablero)) {
return new int[]{x, y};
}
}
}
}
return new int[]{-1, -1};
}
Si te fijas, esa función se llama dos veces en el código para que el CPU elija. La primera vez es para saber si el CPU puede ganar, y la segunda para saber si el oponente gana para poder evitarlo.
Ahora tenemos la función que te dice cuando obtienes el mayor puntaje. Es decir, puede que no ganes, pero al colocar tu pieza en ese lugar podrías obtener un mayor puntaje para ganar.
Se hace lo mismo: invocarla dos veces, una para el CPU y otra para el oponente:
/*
Esta función cuenta y te dice el mayor puntaje, pero no te dice en cuál X ni cuál Y. Está pensada
para ser llamada desde otra función que lleva cuenta de X e Y
*/static int contarSinSaberCoordenadas(char jugador, char[][] copiaTablero) {
int conteoMayor = 0;
int x, y;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
// Colocamos y contamos el puntaje
int conteoTemporal;
conteoTemporal = contarHaciaArriba(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaArribaDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaAbajoDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
}
}
return conteoMayor;
}
/*
Esta función complementa a contarSinSaberCoordenadas. Te dice en qué X e Y el jugador [jugador]
obtendrá el mayor puntaje si pone ahí su pieza
*/static int[] coordenadasParaMayorPuntaje(char jugador, char[][] tableroOriginal) {
int y, x;
int conteoMayor = 0,
xConteoMayor = -1,
yConteoMayor = -1;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
char[][] copiaTablero = clonarMatriz(tableroOriginal);
if (!coordenadasVacias(y, x, copiaTablero)) {
continue;
}
// Colocamos y contamos el puntaje
colocarPieza(y, x, jugador, copiaTablero);
int conteoTemporal = contarSinSaberCoordenadas(jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
yConteoMayor = y;
xConteoMayor = x;
}
}
}
return new int[]{conteoMayor, xConteoMayor, yConteoMayor};
}
Finalmente se elige la esquina superior izquierda o una aleatoria:
// Devuelve coordenadas válidas
static int[] obtenerCoordenadasAleatorias(char jugador, char[][] tableroOriginal) {
int x, y;
do {
x = aleatorioEnRango(0, COLUMNAS - 1);
y = aleatorioEnRango(0, FILAS - 1);
} while (!coordenadasVacias(y, x, tableroOriginal));
return new int[]{x, y};
}
Analizando las demás funciones de Java para este juego de Tres en línea vemos las funciones de conteo que se encargan justamente de contar cuántas piezas están conectadas en una línea.
/*
Funciones de conteo. Simplemente cuentan cuántas piezas del mismo jugador están
alineadas
*/
static int contarHaciaArriba(int x, int y, char jugador, char[][] tablero) {
int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
int contador = 0;
for (; yInicio <= y; yInicio++) {
if (tablero[yInicio][x] == jugador) {
contador++;
} else {
contador = 0;
}
}
return contador;
}
static int contarHaciaDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int contador = 0;
for (; x <= xFin; x++) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
}
return contador;
}
static int contarHaciaArribaDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
int contador = 0;
while (x <= xFin && yInicio <= y) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
x++;
y--;
}
return contador;
}
static int contarHaciaAbajoDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int yFin = (y + CONTEO_PARA_GANAR < FILAS) ? y + CONTEO_PARA_GANAR - 1 : FILAS - 1;
int contador = 0;
while (x <= xFin && y <= yFin) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
x++;
y++;
}
return contador;
}
Se hace un ciclo for para ir de un lugar a otra, sin llegar a índices que no existen. Luego ocupamos estas funciones para saber si hay un ganador:
// Indica si el jugador gana
static boolean comprobarSiGana(char jugador, char[][] tablero) {
int y;
for (y = 0; y < FILAS; y++) {
int x;
for (x = 0; x < COLUMNAS; x++) {
if (
contarHaciaArriba(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaArribaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaAbajoDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR) {
return true;
}
}
}
// Terminamos de recorrer y no conectó
return false;
}
Para terminar veamos las dos funciones que faltan. Son varios métodos que ayudan al programa devolviendo números aleatorios, el oponente de un jugador, indicando si hay un empate, etcétera. Queda así:
// Imprime algo que el CPU "dice"
static void hablar(String mensaje, char jugador) {
System.out.printf("\nCPU (%c) dice: %s\n\n", jugador, mensaje);
}
// Debería llamarse después de verificar si alguien gana
// Indica si hay un empate
static boolean empate(char[][] tableroOriginal) {
int y;
for (y = 0; y < FILAS; y++) {
int x;
for (x = 0; x < COLUMNAS; x++) {
// Si hay al menos un espacio vacío se dice que no hay empate
if (tableroOriginal[y][x] == ESPACIO_VACIO) {
return false;
}
}
}
return true;
}
// Devuelve un número aleatorio en un rango, incluyendo los límites
public static int aleatorioEnRango(int minimo, int maximo) {
// nextInt regresa en rango pero con límite superior exclusivo, por eso sumamos 1
return ThreadLocalRandom.current().nextInt(minimo, maximo + 1);
}
Te dejo el código completo a continuación:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Creado por Parzibyte (https://parzibyte.me).
------------------------------------------------------------------------------------------------
| IMPORTANTE |
Si vas a borrar este encabezado, considera:
Seguirme: https://parzibyte.me/blog/sigueme/
Y compartir mi blog con tus amigos
También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
Twitter: https://twitter.com/parzibyte
Facebook: https://facebook.com/parzibyte.fanpage
Instagram: https://instagram.com/parzibyte
Hacer una donación vía PayPal: https://paypal.me/LuisCabreraBenito
------------------------------------------------------------------------------------------------
* */package me.parzibyte;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
final static int FILAS = 3;
final static int COLUMNAS = 3;
final static char JUGADOR_X = 'X';
final static char JUGADOR_O = 'O';
final static char JUGADOR_CPU_O = JUGADOR_O;
final static char ESPACIO_VACIO = ' ';
final static int CONTEO_PARA_GANAR = 3;
// Modos de juego
final static int JUGADOR_JUGADOR = 1;
final static int JUGADOR_CPU = 2;
final static int CPU_CPU = 3;
// Para leer del teclado
final static Scanner sc = new Scanner(System.in);
// Clona la matriz. Útil para las simulaciones que se hacen, así no se modifica el tablero original
static char[][] clonarMatriz(char[][] tableroOriginal) {
char[][] copia = new char[FILAS][COLUMNAS];
for (int y = 0; y < FILAS; y++) {
for (int x = 0; x < COLUMNAS; x++) {
copia[y][x] = tableroOriginal[y][x];
}
}
return copia;
}
// Establece el tablero en espacios vacíos
static void limpiarTablero(char[][] tablero) {
int y;
for (y = 0; y < FILAS; y++) {
int x;
for (x = 0; x < COLUMNAS; x++) {
tablero[y][x] = ESPACIO_VACIO;
}
}
}
// Imprime el tablero de juego
static void imprimirTablero(char[][] tablero) {
System.out.print("\n");
int y;
int x;
// Imprimir encabezado
System.out.print("| ");
for (x = 0; x < COLUMNAS; x++) {
System.out.printf("|%d", x + 1);
}
System.out.print("|\n");
for (y = 0; y < FILAS; y++) {
System.out.printf("|%d", y + 1);
for (x = 0; x < COLUMNAS; x++) {
System.out.printf("|%c", tablero[y][x]);
}
System.out.print("|\n");
}
}
// Indica si el tablero está vacío en las coordenadas indicadas
static boolean coordenadasVacias(int y, int x, char[][] tablero) {
return tablero[y][x] == ESPACIO_VACIO;
}
// Coloca la X o O en las coordenadas especificadas
static void colocarPieza(int y, int x, char pieza, char[][] tablero) {
if (y < 0 || y >= FILAS) {
System.out.print("Fila incorrecta");
return;
}
if (x < 0 || x >= COLUMNAS) {
System.out.print("Columna incorrecta");
return;
}
if (pieza != JUGADOR_O && pieza != JUGADOR_X) {
System.out.printf("La pieza debe ser %c o %c", JUGADOR_O, JUGADOR_X);
return;
}
if (!coordenadasVacias(y, x, tablero)) {
System.out.print("Coordenadas ya ocupadas");
return;
}
tablero[y][x] = pieza;
}
/*
Funciones de conteo. Simplemente cuentan cuántas piezas del mismo jugador están
alineadas
*/
static int contarHaciaArriba(int x, int y, char jugador, char[][] tablero) {
int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
int contador = 0;
for (; yInicio <= y; yInicio++) {
if (tablero[yInicio][x] == jugador) {
contador++;
} else {
contador = 0;
}
}
return contador;
}
static int contarHaciaDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int contador = 0;
for (; x <= xFin; x++) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
}
return contador;
}
static int contarHaciaArribaDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
int contador = 0;
while (x <= xFin && yInicio <= y) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
x++;
y--;
}
return contador;
}
static int contarHaciaAbajoDerecha(int x, int y, char jugador, char[][] tablero) {
int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
int yFin = (y + CONTEO_PARA_GANAR < FILAS) ? y + CONTEO_PARA_GANAR - 1 : FILAS - 1;
int contador = 0;
while (x <= xFin && y <= yFin) {
if (tablero[y][x] == jugador) {
contador++;
} else {
contador = 0;
}
x++;
y++;
}
return contador;
}
// Indica si el jugador gana
static boolean comprobarSiGana(char jugador, char[][] tablero) {
int y;
for (y = 0; y < FILAS; y++) {
int x;
for (x = 0; x < COLUMNAS; x++) {
if (
contarHaciaArriba(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaArribaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
contarHaciaAbajoDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR) {
return true;
}
}
}
// Terminamos de recorrer y no conectó
return false;
}
// Devuelve el jugador contrario al que se le pasa. Es decir, le das un O y te devuelve el X
static char oponenteDe(char jugador) {
if (jugador == JUGADOR_O) {
return JUGADOR_X;
} else {
return JUGADOR_O;
}
}
// Imprime algo que el CPU "dice"
static void hablar(String mensaje, char jugador) {
System.out.printf("\nCPU (%c) dice: %s\n\n", jugador, mensaje);
}
// Debería llamarse después de verificar si alguien gana
// Indica si hay un empate
static boolean empate(char[][] tableroOriginal) {
int y;
for (y = 0; y < FILAS; y++) {
int x;
for (x = 0; x < COLUMNAS; x++) {
// Si hay al menos un espacio vacío se dice que no hay empate
if (tableroOriginal[y][x] == ESPACIO_VACIO) {
return false;
}
}
}
return true;
}
// Devuelve un número aleatorio en un rango, incluyendo los límites
public static int aleatorioEnRango(int minimo, int maximo) {
// nextInt regresa en rango pero con límite superior exclusivo, por eso sumamos 1
return ThreadLocalRandom.current().nextInt(minimo, maximo + 1);
}
// Devuelve coordenadas válidas
static int[] obtenerCoordenadasAleatorias(char jugador, char[][] tableroOriginal) {
int x, y;
do {
x = aleatorioEnRango(0, COLUMNAS - 1);
y = aleatorioEnRango(0, FILAS - 1);
} while (!coordenadasVacias(y, x, tableroOriginal));
return new int[]{x, y};
}
// Devuelve las coordenadas en las que se puede ganar, o -1 y -1 si no se puede ganar
static int[] coordenadasParaGanar(char jugador, char[][] tableroOriginal) {
int y, x;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
char[][] copiaTablero = clonarMatriz(tableroOriginal);
if (coordenadasVacias(y, x, copiaTablero)) {
colocarPieza(y, x, jugador, copiaTablero);
if (comprobarSiGana(jugador, copiaTablero)) {
return new int[]{x, y};
}
}
}
}
return new int[]{-1, -1};
}
/*
Esta función cuenta y te dice el mayor puntaje, pero no te dice en cuál X ni cuál Y. Está pensada
para ser llamada desde otra función que lleva cuenta de X e Y
*/ static int contarSinSaberCoordenadas(char jugador, char[][] copiaTablero) {
int conteoMayor = 0;
int x, y;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
// Colocamos y contamos el puntaje
int conteoTemporal;
conteoTemporal = contarHaciaArriba(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaArribaDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
conteoTemporal = contarHaciaAbajoDerecha(x, y, jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
}
}
}
return conteoMayor;
}
/*
Esta función complementa a contarSinSaberCoordenadas. Te dice en qué X e Y el jugador [jugador]
obtendrá el mayor puntaje si pone ahí su pieza
*/ static int[] coordenadasParaMayorPuntaje(char jugador, char[][] tableroOriginal) {
int y, x;
int conteoMayor = 0,
xConteoMayor = -1,
yConteoMayor = -1;
for (y = 0; y < FILAS; y++) {
for (x = 0; x < COLUMNAS; x++) {
char[][] copiaTablero = clonarMatriz(tableroOriginal);
if (!coordenadasVacias(y, x, copiaTablero)) {
continue;
}
// Colocamos y contamos el puntaje
colocarPieza(y, x, jugador, copiaTablero);
int conteoTemporal = contarSinSaberCoordenadas(jugador, copiaTablero);
if (conteoTemporal > conteoMayor) {
conteoMayor = conteoTemporal;
yConteoMayor = y;
xConteoMayor = x;
}
}
}
return new int[]{conteoMayor, xConteoMayor, yConteoMayor};
}
// Hace que el CPU elija unas coordenadas para ganar
static int[] elegirCoordenadasCpu(char jugador, char[][] tablero) {
hablar("Estoy pensando...", jugador);
/*
El orden en el que el CPU infiere las coordenadas que toma es:
1. Ganar si se puede
2. Hacer perder al oponente si está a punto de ganar
3. Tomar el mejor movimiento del oponente (en donde obtiene el mayor puntaje)
4. Tomar mi mejor movimiento (en donde obtengo mayor puntaje)
5. Elegir la de la esquina superior izquierda (0,0)
6. Coordenadas aleatorias
*/ int y, x, conteoJugador, conteoOponente;
int yOponente, xOponente;
int[] coordenadas = new int[2];
char oponente = oponenteDe(jugador);
// 1
coordenadas = coordenadasParaGanar(jugador, tablero);
x = coordenadas[0];
y = coordenadas[1];
if (y != -1 && x != -1) {
hablar("Ganar", jugador);
return new int[]{x, y};
}
// 2
coordenadas = coordenadasParaGanar(oponente, tablero);
x = coordenadas[0];
y = coordenadas[1];
if (y != -1 && x != -1) {
hablar("Tomar victoria de oponente", jugador);
return new int[]{x, y};
}
// 3
int[] coordenadasJugador = coordenadasParaMayorPuntaje(jugador, tablero);
int[] coordenadasOponente = coordenadasParaMayorPuntaje(oponente, tablero);
conteoJugador = coordenadasJugador[0];
x = coordenadasJugador[1];
y = coordenadasJugador[2];
conteoOponente = coordenadasOponente[0];
xOponente = coordenadasOponente[1];
yOponente = coordenadasOponente[2];
if (conteoOponente > conteoJugador) {
hablar("Tomar puntaje mayor del oponente", jugador);
return new int[]{xOponente, yOponente};
} else if (conteoJugador > 0) {
hablar("Tomar mi mayor puntaje", jugador);
return new int[]{x, y};
}
// 4
if (coordenadasVacias(0, 0, tablero)) {
hablar("Tomar columna superior izquierda", jugador);
return new int[]{0, 0};
}
// 5
hablar("Coordenadas aleatorias", jugador);
coordenadas = obtenerCoordenadasAleatorias(jugador, tablero);
return coordenadas;
}
// Devuelve un jugador aleatorio
static char jugadorAleatorio() {
if (aleatorioEnRango(0, 1) == 0) {
return JUGADOR_O;
} else {
return JUGADOR_X;
}
}
// Loop principal del juego
static void iniciarJuego(int modo) {
if (modo != JUGADOR_JUGADOR && modo != JUGADOR_CPU && modo != CPU_CPU) {
System.out.print("Modo de juego no permitido");
return;
}
// Para que salgan cosas aleatorias
// Iniciar tablero de juego
char[][] tablero = new char[FILAS][COLUMNAS];
// Y limpiarlo
limpiarTablero(tablero);
// Elegir jugador que inicia al azar
char jugadorActual = jugadorAleatorio();
System.out.printf("El jugador que inicia es: %c\n", jugadorActual);
int x = 0, y = 0;
// Y allá vamos
int[] coordenadas = new int[2];
while (true) {
imprimirTablero(tablero);
if (modo == JUGADOR_JUGADOR || (modo == JUGADOR_CPU && jugadorActual == JUGADOR_X)) {
System.out.printf("Jugador %c. Ingresa coordenadas (x,y) para colocar la pieza\n", jugadorActual);
do {
x = solicitarNumeroValido("Ingresa X: ", 1, COLUMNAS);
y = solicitarNumeroValido("Ingresa Y: ", 1, FILAS);
if (!coordenadasVacias(y - 1, x - 1, tablero)) {
System.out.println("Coordenadas ocupadas. Intenta de nuevo");
}
} while (!coordenadasVacias(y - 1, x - 1, tablero));
// Al usuario se le solicitan números comenzando a contar en 1, pero en los arreglos comenzamos desde el 0
// así que necesitamos restar uno en ambas variables
x--;
y--;
} else if (modo == CPU_CPU || jugadorActual == JUGADOR_CPU_O) {
// Si es modo CPU contra CPU o es el turno del CPU, dejamos que las coordenadas las elija
// el programa
coordenadas = elegirCoordenadasCpu(jugadorActual, tablero);
x = coordenadas[0];
y = coordenadas[1];
}
// Sin importar cuál modo haya sido, colocamos la pieza según las coordenadas elegidas
colocarPieza(y, x, jugadorActual, tablero);
// Puede que después de colocar la pieza el jugador gane o haya un empate, así que comprobamos
if (comprobarSiGana(jugadorActual, tablero)) {
imprimirTablero(tablero);
System.out.printf("El jugador %c gana\n", jugadorActual);
return;
} else if (empate(tablero)) {
imprimirTablero(tablero);
System.out.print("Empate");
return;
}
// Si no, es turno del otro jugador
jugadorActual = oponenteDe(jugadorActual);
}
}
public static int solicitarNumeroValido(String mensaje, int minimo, int maximo) {
int numero;
while (true) {
System.out.println(mensaje);
if (sc.hasNextInt()) {
numero = sc.nextInt();
if (numero >= minimo && numero <= maximo) {
return numero;
} else {
System.out.println("Número fuera de rango. Intente de nuevo");
}
} else {
sc.next();
}
}
}
public static void main(String[] args) {
int modo;
String menu = "1. Humano contra humano\n2. Humano contra CPU (El CPU juega como " + JUGADOR_CPU_O + ")\n3. CPU contra CPU\nElige: ";
modo = solicitarNumeroValido(menu, 1, 3);
iniciarJuego(modo);
sc.close();
}
}
Si quieres puedes importarlo a NetBeans, Eclipse, IntelliJ IDEA, etcétera. También te cuento que ya he programado este juego en C.
Finalmente te dejo con más cosas interesantes sobre Java en el blog de Parzibyte.
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.