Tutorial: Gomoku en Java (Parte 1)

Reglas Gomoku

El gomoku o Go-moku o Cinco en líneas es un juego tradicional de origen oriental que tiene como antecedente el juego Go (que se conoce desde hace 4000 años en China) Otros datos dicen que el juego de mesa Gomoku aparece en Japón con el nombre de Kakugo (que significa algo así como "cinco pasos" en japonés), y pronto se convirtió en un pasatiempo nacional. El juego de mesa Gomoku se introdujo en Europa alrededor de 1885 y llegó a ser conocido en Inglaterra con el nombre de Cinco Disfrute.


Objetivo del juego

El objetivo del juego es hacer una línea horizontal, vertical o diagonal con 5 fichas antes que el oponente.


Cómo jugar Gomoku


En este desarrollo se juega el Gomoku en un tablero de 10 x 10 (100 bloques) y fichas Negras y Blancas. El jugador que lleva la ficha Negra empieza el juego; cada jugador hará un movimiento por turno. El movimiento implica poner una ficha en un cuadro libre del tablero. Las piezas colocadas no se pueden mover de nuevo. Gana la partida el jugador que logra alinear 5 fichas consecutivas.


Estructura del proyecto



Este proyecto ha sido desarrollado utilizando el patrón de arquitectura MVC (Modelo-Vista-Controlador). El código fuente descargable lo encontrarás al final del tutorial.

A continuación una breve descripción de qué hace cada clase:

  • controller (paquete)
    • MyActionListener.java.- Lleva el conteo de los movimientos de los jugadores, sirve de puente entre las clases de la vista y las clases del modelo, controla acciones realizadas por los jugadores en el front-end para responder con otra acción como mensajes de movimientos erroneos, o quién es el ganador.
  • model (paquete)
    • Board.java.- Resprensentación de una tabla de gomoku, siguiendo la lógica de que la tabla contiene una lista de bloques, los cuales pueden contener fichas negras o blancas. La tabla debe contener las reglas de la tabla, como por ejemplo, no colocar una ficha sobre un bloque ya ocupado, validar si ya existe un 5 en fila.
    • Game.java.- Contiene funciones o métodos mas concretos del juego para invocarlos desde el controlador, como por ejemplo, ubicar una ficha, comenzar un nuevo juego.
    • Square.java.- Representación de un bloque de los 100 que existen en una tabla de gomoku. Es un simple botón que tiene propiedades agregadas como posciónX, posiciónY, para saber dónde se ha ubicado la ficha y un método para cambiar el color del bloque según la ficha que se colocó encima.
    • StoneColor.java.- Enumerador, sirve para representar los colores que puede tener un bloque, White si hay una ficha blanca sobre esta, Black si hay una ficha negra sobre esta, y Empty si está vacia. 
  • view (paquete)
    • GomokuFrame.java.- El front-end, en este tutorial se ha programado a mano el diseño.
    • Main.java.- La clase principal, aqui se puede cambiar el tamaño de la tabla, por defecto está en 10 x 10.


Desarrollo

Lo primero será crear los paquetes y clases que se muestran en la imagen referente a la estructura del proyecto. Puedes hacerlo con click derecho sobre el proyecto->New->Package/Class


Una vez tengamos la estructura del proyecto como en la imagen, proceder con trabajar cada clase:

StoneColor.java


Cambiamos de clase a Enumerador, agregamos los items White, Black y Empty:


package model;

public enum StoneColor
{
 Black,White,Empty
}


Square.java


Recordando que esta clase será la representación de un bloque de la tabla, hacemos herencia de la clase JButton, (no olvidar importar la libreria swing).

Agregamos los atributos positionOnBoardX y positionOnBoardY para saber las coordenadas del bloque en la tabla, por ejemplo fila 5, columna 8.

Agregamos un atributo stone de tipo StoneColor, el cual es el enumerador definido anteriormente. significando que la piedra o fiicha podrá ser de color Black, White, Empty.

Agregamos metodos get y set para jugar con los atributos.

Agregamos un método cleanSquare para limpiar el bloque cuando se reinicie el juego.

Agregamos un método setStoneOverMe que servirá para emular el posicionamiento de una ficha sobre el bloque.

Nota: En esta clase no hay validaciones de ningún tipo.


package model;

import java.awt.Color;
import javax.swing.JButton;

public class Square extends JButton{
 private int positionOnBoardX;
 private int positionOnBoardY;
 private StoneColor stone;
 private Color emptySquareColor = new Color(108,99,235);
 
 public Square(int positionOnBoardX, int positionOnBoardY) {
  super();
  this.positionOnBoardX = positionOnBoardX;
  this.positionOnBoardY = positionOnBoardY;
  this.stone = StoneColor.Empty;
  this.setBackground(emptySquareColor);
 }

 public int getPositionOnBoardX() {
  return positionOnBoardX;
 }

 public int getPositionOnBoardY() {
  return positionOnBoardY;
 }
 
 public void setStoneOverMe(StoneColor color) {
  this.stone = color;
  this.setBackground((color == StoneColor.Black)?Color.BLACK:Color.WHITE);
 }
 
 public void cleanSquare() {
  this.stone = StoneColor.Empty;
  this.setBackground(emptySquareColor);
 }

 public StoneColor getStoneColor() {
  return stone;
 }
}


Board.java


esta clase contiene casi toda la lógica y validaciones del proyecto, trataré de explicar lo más que pueda.

Atributos:

  • listSquares.- Una lista de Square, contiene todos los bloques de la tabla.
  • listBlackStones.- Una lista de Square, contiene solo bloques de color Black.
  • listWhiteStones.- Una lista de Square, contiene solo bloques de color White.
  • numberOfCols.- Número de columnas de la tabla.
  • numberOfRows.- Número de filas de la tabla.
  • isThereAWinner.- Booleano para saber si hay un ganador.
  • chainsFound.- Lista de cadenas de fichas encontradas, por ejemplo tenemos 3 fichas negras consecutivas en un sector de la tabla, en otro sector tenemos 4 fichas negras consecutivas, esas cadenas es lo que va a almacenar este atributo con el fin de saber si existe una cadena de 5 fichas consecutivas y poder determinar posteriormente el ganador.


Constructor:
Aquí inicializamos nuestros atributos, y creamos la tabla con sus bloques filas x columnas.

Métodos:

  • collectHorizontalChains.- Colecciona todas las cadenas horizontales de la tabla de un determinado color de fichas.
  • collectVerticalChains.- Colecciona todas las cadenas verticales de la tabla de un determinado color de fichas.
  • collectDiagonallyRightChains.- Colecciona todas las cadenas diagonal-derecho de la tabla de un determinado color de fichas.
  • collectDiagonallyLeftChains.- Colecciona todas las cadenas diagonal-izquierda de la tabla de un determinado color de fichas.
  • isThereAFiveChain.- Colecciona todas las cadenas en todas las direcciones (horizontal, vertical, diagonal), para luego almanarlas en el atributo chainsFound, y posteriormente preguntar si alguna de las cadenas encontradas tiene una longitud igual a 5 fichas, y así cambiamos el valor del atributo booleano isThereAWinner a True.
  • getSquareAtRowCol.- Obtiene un bloque dada su posición fila-columna.
  • place.- Posiciona una ficha sobre un bloque dada su posición fila-columna y color de ficha a ubicar.
  • cleanTheBoard.- Limpia toda la tabla para iniciar un nuevo juego.
  • isThereAtLeastOneSquareEmpty.- método booleano para saber si hay al menos un bloque vacío, significando que aún se puede realizar un movimiento, por False significa que no hay más movimientos disponibles.
  • cleanOneSquare.- Limpia un bloque.
  • isCorrectStep.- Método booleano para saber si el movimiento realizado es válido o no, para ello el bloque debe estar vacío o Empty.
  • getListSquares.- Obtiene la lista de todos los bloques de la tabla.



package model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Board {
 private List<Square> listSquares;
 private List<Square> listBlackStones;
 private List<Square> listWhiteStones;
 private int numberOfCols;
 private int numberOfRows;
 private boolean isThereAWinner;
 private List<Integer> chainsFound;
 
 public Board(int numberOfRows, int numberOfCols) {
  this.isThereAWinner = false;
  this.listSquares = new ArrayList<Square>();
  this.listBlackStones = new ArrayList<Square>();
  this.listWhiteStones = new ArrayList<Square>();
  this.chainsFound = new ArrayList<Integer>();
  this.numberOfCols = numberOfCols;
  this.numberOfRows = numberOfRows;
  
  //Creating the Board of size: Rows x Cols
  for (int row = 1; row <= numberOfRows; row++) {
   for (int col = 1; col <= numberOfCols; col++) {
    this.listSquares.add(new Square(row, col));
   }
  }
 }
 
 public boolean isThereAFiveChain(StoneColor stoneColor)
 {
  this.collectHorizontalChains(stoneColor);
  this.collectVerticalChains(stoneColor);
  this.collectDiagonallyRightChains(stoneColor);
  this.collectDiagonallyLeftChains(stoneColor);
  
  if (this.chainsFound.size()>0)
  {
   switch (Collections.max(this.chainsFound)) {
   case 3:
    break;
   case 4:
    break;
   case 5:
    this.isThereAWinner = true;
    break;
   default:
    break;
   }
  }
  
  this.chainsFound.clear();
  return this.isThereAWinner;
 }
 
 private void collectHorizontalChains(StoneColor stoneColor)
 {
  int count = 0;
     for (int row = 1; row <=this.numberOfRows; row++) {
         for (int column = 1; column <= this.numberOfCols +1; column++) 
         {
          Square s= this.getSquareAtRowCol(row,column);
             if (s != null && s.getStoneColor() == stoneColor && count < 5)
                 count++;
             else if(count >= 3){
               this.chainsFound.add(count);
               count = 0;
    }else{
                  count = 0;
     }
         }
     }
    }
 
 private void collectVerticalChains(StoneColor stoneColor)
 {
  int count = 0;
     for (int row = 1; row <=this.numberOfRows; row++) {
         for (int column = 1; column <= this.numberOfCols +1; column++) 
         {
          Square s= this.getSquareAtRowCol(column,row);
             if (s != null && s.getStoneColor() == stoneColor && count < 5)
                 count++;
             else if(count >= 3){
               this.chainsFound.add(count);
               count = 0;
    }else{
                  count = 0;
     }
         }
     }
 }

 private void collectDiagonallyRightChains(StoneColor stoneColor)
 {
  int count = 0;
     int rowDinamic=1;
     int row = 0,column=0;
     
     for (row = 1; row <=this.numberOfRows; row++) 
     {
      rowDinamic = row;
         for (column = 1; column <= this.numberOfCols + 1; column++) {
          Square s= this.getSquareAtRowCol(column,row);
          if (rowDinamic <= this.numberOfRows)
              if (s != null && this.getSquareAtRowCol(rowDinamic,column).getStoneColor() == stoneColor && count < 5)
              {
               count++;
               rowDinamic++;
              }else if(count >= 3){
               this.chainsFound.add(count);
               count = 0;
              }else{
               rowDinamic = row;
                  count = 0;
     }
         }
     }
    }
 
 private void collectDiagonallyLeftChains(StoneColor stoneColor)
 {
  int count = 0;
     int rowDinamic=1;
     int row = 0,column=0;
     
     for (row = 1; row <=this.numberOfRows; row++) 
     {
      rowDinamic = row;
         for (column = this.numberOfCols; column >= 0; column--) {
          Square s= this.getSquareAtRowCol(column,row);
          if (rowDinamic <= this.numberOfRows)
              if (s != null && this.getSquareAtRowCol(rowDinamic,column).getStoneColor() == stoneColor && count < 5)
              {
               count++;
               rowDinamic++;
              }else if(count >= 3){
               chainsFound.add(count);
               count = 0;
     }else{
      isThereAWinner = false;
               rowDinamic = row;
                  count = 0;
     }
         }
     }
    }
 
 private Square getSquareAtRowCol(int row, int col){
  for (Square square : listSquares) {
   if (square.getPositionOnBoardX() == row && square.getPositionOnBoardY() == col)
    return square;
  }
  return null;
 }

 public void place(int row, int col, StoneColor stoneColor) 
 {
  Square s = this.getSquareAtRowCol(row,col);
  s.setStoneOverMe(stoneColor);
  if(stoneColor == StoneColor.Black) this.listBlackStones.add(s);
  if(stoneColor == StoneColor.White) this.listWhiteStones.add(s);
 }
 
 public void cleanTheBoard() 
 {
  for (Square square : this.listSquares)
   square.cleanSquare();
  
  this.listBlackStones.clear();
  this.listWhiteStones.clear();
  this.chainsFound.clear();
  this.isThereAWinner = false;
 }
 
 public boolean isThereAtLeastOneSquareEmpty() 
 {
  for (Square square : this.listSquares)
  {
   if(square.getStoneColor() == StoneColor.Empty)
    return true;
  }
  return false;
 }
 
 public void cleanOneSquare(int row, int col) 
 {
  Square s = getSquareAtRowCol(row, col);
  s.cleanSquare();
 }
 
 public boolean isCorrectStep(int row, int col){
  return (this.getSquareAtRowCol(row,col).getStoneColor() == StoneColor.Empty);
 }

 public List<Square> getListSquares() {
  return listSquares;
 }
}



Continua en la parte 2

Para ver la segunda parte de este tutorial da click en el siguiente enlace:

Comentarios