Bomba Buscaminas

Aug 28 2020

Tengo un método que comprueba todos los cuadrados circundantes y devuelve el número de bombas a su alrededor. Pero es un código realmente largo y feo, entonces, ¿se puede acortar?

final int MINE =10

for (int x = 0; x < counts.length; x++) {
    for (int y = 0; y < counts[0].length; y++) {
    if (counts[x][y] != MINE) {
    int Minesearch = 0;
    if (x > 0 && y > 0 && counts[x-1][y-1] == MINE) {//up left
        Minesearch++;
    }
    if (y > 0 && counts[x][y-1] == MINE) {//up
        Minesearch++;
    }
    if (x < counts.length - 1 && y > 0 && counts[x+1][y-1] == MINE) {//up right
        Minesearch++;
    }
    if (x > 0 && counts[x-1][y] == MINE) {//left
        Minesearch++;
    }
    if (x < counts.length - 1 && counts[x+1][y] == MINE) {//right
        Minesearch++;
    }
    if (x > 0 && y < counts[0].length - 1 && counts[x-1][y+1] == MINE) {//down left
        Minesearch++;
    }
    if (y < counts[0].length - 1 && counts[x][y+1] == MINE) {//down
        Minesearch++;
    }
    if (x < counts.length - 1 && y < counts[0].length - 1 && counts[x+1][y+1] == MINE) {//down right
        Minesearch++;
    }
    counts[x][y] = Minesearch;
    }
    }
}
}

Respuestas

12 TorbenPutkonen Aug 28 2020 at 12:28

Se puede acortar y "embellecer" refactorizando el control de límites y el control de minas en el método interno:

private boolean isWithinBounds(int x, int y) {
    return x >= 0 && y >= 0 && x < width && y < height;
}

private boolean isMine(int x, int y) {
    return field[x][y] == MINE;
}

Entonces, el recuento de minas se vuelve trivial (podemos asumir que el centro del cuadrado de 3x3 no tiene una mina, de lo contrario, el jugador habría explotado y habría terminado el juego):

for (int x1 = x - 1; x1 <= x + 1; x1++) {
    for (int y1 = y - 1; y1 <= y + 1; y1++) {
        if (isWithinBounds(x1, y1) && isMine(x1, y1) {
            mineCount++;
        }
    }
}

Lo que hemos hecho es dividir el código en métodos, cada uno de los cuales implementa una función pequeña y bien definida. Debido a que cada método hace exactamente una cosa, se vuelven más fáciles de entender, mantener y probar.

Debe verificar las convenciones de nomenclatura de Java . Los nombres de variables deben estar en camelCase, startingWithSmallLetter.

Los nombres de variables y métodos deben describir la razón por la que existe el código. Por ejemplo, mineSearches confuso ya que la variable no busca minas, solo las cuenta. Por tanto, mineCountes una mejor alternativa.

Countstambién es confuso ya que contiene un valor nombrado MINEque obviamente es un marcador para una celda que contiene el mío, pero también contiene los recuentos de minas circundantes. Hice un clon de buscaminas (o en realidad un solucionador de buscaminas) una vez y usé una matriz que contiene Cell-objects. El objeto Cell proporcionó métodos para consultar el estado de la celda (pisado, marcado, desconocido) y el número de minas circundantes si se hubiera pisado.

1 TimothyTruckle Aug 29 2020 at 20:29

Si bien la respuesta de @TorbenPutkonen es correcta, es un enfoque de procedimiento al problema.

No hay nada de malo con los enfoques procedimentales como tales, pero dado que Java es un lenguaje orientado a objetos, podríamos buscar enfoques OO en su lugar ...

Extraería el cheque vecino en un enumaspecto como este:

enum Direction {
  NORTH{
     boolean isBomb(inx x, int y, boolean[] field){
       if(0 < x)
         return BOMB == field(x-1, y);
       else
         return false;
     }
  },
  NORTH_WEST{
     boolean isBomb(inx x, int y, boolean[] field){
       if(0 < x && 0 < y)
         return BOMB == field(x-1, y-1);
       else
         return false;
     }
  },
  SOUTH{
     boolean isBomb(inx x, int y, boolean[] field){
       if(field.length-1 > x)
         return BOMB == field(x+1, y);
       else
         return false;
     }
  },
  SOUTH_EAST{
     boolean isBomb(inx x, int y, boolean[] field){
       if(field.length-1 > x && field[0].length-1>y)
         return BOMB == field(x+1, y+1);
       else
         return false;
     }
  }
  // other directions following same pattern

  abstract boolean isBomb(inx x, int y, boolean[] field);
}

El beneficio es que esta enumeración podría vivir en su propio archivo y tiene una responsabilidad muy limitada. Eso significa que es fácil entender lo que hace, ¿no?

En su método de cálculo, simplemente puede iterar sobre las enumconstantes de esta manera:

for (int x = 0; x < counts.length; x++) {
  for (int y = 0; y < counts[0].length; y++) {
    int mineCount =0;
    for(Direction direction : Direction.values()) {
       if (direction.isBomb(x, y, counts) ) {
            mineCount++;
       }
    }
  }
}

Como siguiente paso, aplicaría el principio de "decir, no preguntar" cambiando la firma del método:

abstract int getBombValueOf(inx x, int y, boolean[] field);

la implementación en el enumcambiaría así:

     int getBombValueOf(inx x, int y, boolean[] field){
       if(0 < x && BOMB == field(x-1, y))
         return 1;
       else
         return 0;
     },

Eso podría simplificarse al "operador de elvis":

     int getBombValueOf(inx x, int y, boolean[] field){
       return (0 < x && BOMB == field(x-1, y))
         ? 1 
         : 0;
     },

y el uso cambiaría a esto:

for (int x = 0; x < counts.length; x++) {
  for (int y = 0; y < counts[0].length; y++) {
    int mineCount =0;
    for(Direction direction : Direction.values()) {
       mineCount += 
           direction.getBombValueOf(x, y, counts) );
    }
  }
}

Podríamos lograr lo mismo (excepto moviendo el cálculo vecino a otro archivo) usando una FunctionalInterfacey una colección simple:

@FunctionalInterface
interface Direction{
  int getBombValueOf(inx x, int y, boolean[] field);
}

private final Collection<Direction> directions = new HashSet<>();

// in constructor
   directions.add(new Direction() { // old style anonymous inner class
         int getBombValueOf(inx x, int y, boolean[] field){
           return (0 < x && BOMB == field(x-1, y))
             ? 1 
             : 0;           
         }
    };
   directions.add((x, y, field)-> { // Java8 Lambda 
           return (0 < x && 0 < y &&BOMB == field(x-1, y-1))
             ? 1 
             : 0;
    };
    // more following same pattern

// in your method
    for (int x = 0; x < counts.length; x++) {
      for (int y = 0; y < counts[0].length; y++) {
        int mineCount =0;
        for(Direction direction : directions) {
           mineCount += 
               direction.getBombValueOf(x, y, counts) );
        }
      }
    }

Por supuesto, podríamos sacar mucho más provecho de los principios de OO si el campo del juego no fuera una serie de primitivas sino una colección de objetos . Pero eso podría ser materia para otra respuesta ...; o)