Game of Life (GoL)

Hola chicos, hoy vengo a compartir con ustedes mi implementación del juego de la vida o game of life (GoL).

video: http://youtu.be/TNhex7-0vXU

Me anime a hacerlo despues de haber asistido al Code Retreat que se organizo el pasado sábado 25 de Agosto (Méx. DF.), donde una parte de la dinámica consistía en borrar nuestro código en cada iteración para volverlo a escribir, pero como no escribí ni una línea; porque alguien se atasco con el teclado, pues me di ala tarea de echarmelo por puritito gusto :).

Hacer este ejercicio fue muy rico por lo que me dejó la práctica y por el montón de patrones que aparecen a cada rato en la codificación, es muy interesante ver que el código va creciendo solito, hasta las pruebas unitarias tenían un toque especial; jojojo ya me emocione no!!.

Lo comparto con la esperanza de poder aprender más de la comunidad, como por ejemplo si alguien me pudiera dar un consejo de como deshacerme de esos infernales 8 ‘ifs’ en la clase ‘Board.java’.

Instrucciones de uso:
1. Bajate & instala git.
2. (opcional/recomendable) Crea una cuenta en github.
3. Clona mi repo. (clone https://github.com/rodrigoSaladoAnaya/GameOfLife.git)
3.1. Pon tu usuario y clave.
4. Corre las pruebas de ‘CellTest’ y las de ‘BoardTest’
4.1 Respira ya casi terminamos.
5. Compila el proyecto ‘GameOfLife’ que de el depende el que sigue :P.
6. Compila el proyecto ‘Play’. Aguas que esta es la parte gráfica del programa y esta en JavaFx 2.2, así que necesitarás Java7, Y pues si necesitaste descargar Java7, pues que bueno ya era hora.
6.1. Picale Run a Play.
7. ‘Play’ tiene un método llamado ‘iniBoard’ hay veras una matriz llena de L y D, juegale a los patrones y ve que la parte más perra de todo el programa esta en crear un patrón bonito.

En fin espero que lo disfruten tanto como yo : )

package game;

/**
 * @author rodrigo salado anaya
 */

public final class Cell {

    private final static int size = 3;
    private final static boolean LIVE = true;
    private final static boolean DEAD = false;
    private boolean[][] BOARD;

    public boolean getCellHealth(boolean[] neighbours) {
        fillNeighbours(neighbours);
        return this.applyRules(getNeighboursCount(), getCellHealth());
    }

    protected boolean applyRules(int neighbours, boolean cellHealth) {
        if (cellHealth == LIVE && neighbours < 2) {
            return DEAD;
        }
        if (cellHealth == LIVE && neighbours > 3) {
            return DEAD;
        }
        if (cellHealth == DEAD && neighbours != 3) {
            return DEAD;
        }
        return LIVE;
    }

    protected void fillNeighbours(boolean[] cellsHealth) {
        BOARD = new boolean[size][size];
        int cellPosition = 0;
        for (int i = 0; i != size; i++) {
            for (int j = 0; j != size; j++) {
                BOARD[i][j] = cellsHealth[cellPosition++];
            }
        }
    }

    protected int getNeighboursCount() {
        int count = 0;
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (i == 1 && j == 1) {
                    continue;
                }
                count = BOARD[i][j] ? ++count : count;
            }
        }
        return count;
    }

    protected boolean getCellHealth() {
        return BOARD[1][1];
    }

    protected void setCellHealt(boolean cellHealth) {
        BOARD[1][1] = cellHealth;
    }
}

package game;

/**
 * @author rodrigo salado anaya
 */

public class Board {

    private static int LENGTH;
    private static boolean[][] BOARD;

    private boolean[] setNeighboursValues(
            int sec0, int sec1, int sec2,
            int pos0, int pos1, int pos2) {

        int boardSize = 9;
        boolean[] neighbours = new boolean[boardSize];
        neighbours[0] = BOARD[sec0][pos0];
        neighbours[1] = BOARD[sec0][pos1];
        neighbours[2] = BOARD[sec0][pos2];

        neighbours[3] = BOARD[sec1][pos0];
        neighbours[4] = BOARD[sec1][pos1];
        neighbours[5] = BOARD[sec1][pos2];

        neighbours[6] = BOARD[sec2][pos0];
        neighbours[7] = BOARD[sec2][pos1];
        neighbours[8] = BOARD[sec2][pos2];
        return neighbours;
    }

    protected boolean[] getNeighboursByCell(int x, int y) {
        int length = LENGTH - 1;

        if (x == 0 && y == 0) {
            return setNeighboursValues(length, 0, 1, length, 0, 1);
        }

        if (x == 0 && y == length) {
            return setNeighboursValues(length, 0, 1, length - 1, length, 0);
        }

        if (x == length && y == 0) {
            return setNeighboursValues(length - 1, length, 0, length, 0, 1);
        }

        if (x == length && y == length) {
            return setNeighboursValues(length - 1, length, 0, length - 1, length, 0);
        }

        if (x > 0 && x < length && y == 0) {
            return setNeighboursValues(x - 1, x, x + 1, length, 0, 1);
        }

        if (x > 0 && x < length && y == length) {
            return setNeighboursValues(x - 1, x, x + 1, length - 1, length, 0);
        }

        if (x == length && y > 0 && y < length) {
            return setNeighboursValues(length - 1, length, 0, y - 1, y, y + 1);
        }

        if (x == 0 && y > 0 && y < length) {
            return setNeighboursValues(length, x, x + 1, y - 1, y, y + 1);
        }
        return setNeighboursValues(x - 1, x, x + 1, y - 1, y, y + 1);
    }

    public void nextStep() {
        boolean[][] nextBoard = new boolean[LENGTH][LENGTH];
        for (int i = 0; i != LENGTH; i++) {
            for (int j = 0; j != LENGTH; j++) {
                Cell cell = new Cell();
                boolean[] neighbours = this.getNeighboursByCell(i, j);
                nextBoard[i][j] = cell.getCellHealth(neighbours);
            }
        }
        BOARD = nextBoard;
    }

    public void setBoardLength(int length) {
        LENGTH = length;
        BOARD = new boolean[LENGTH][LENGTH];
    }

    public void fillBoard(boolean[][] board) {
        BOARD = board;
    }

    public boolean[][] getActualBoard() {
        return BOARD;
    }
}

package gameOfLife;

import game.Board;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;

/**
 * @author rodrigo salado anaya
 */

public class Play extends Application {

    private static final boolean L = true; //LIVE
    private static final boolean D = false; //DEAD
    private static final int tick = 100;
    private static final String CELL_LIVE_CHAR = " X ";
    private static final String CELL_DEAD_CHAR = "   ";
    private static AnimationTimer timer;
    private static final Board p = new Board();
    private static int LENGTH;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Group root = new Group();
        primaryStage.setResizable(false);
        primaryStage.setScene(new Scene(root, 220, 245));
        final Text text = new Text();
        text.setFont(new Font("Courier", 12.0));
        timer = getTimer(text);
        root.getChildren().add(text);
        primaryStage.show();
        iniBoard();
        timer.start();
    }

    public void iniBoard() {

        boolean[][] initialBoard = {
            {L, D, L, D, D, L, D, D, L, D, D, L, D, D, L, D, L},
            {D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D},
            {L, D, L, D, L, L, L, D, D, D, L, L, L, D, L, D, L},
            {D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, D, D, L, L, L, D, D, D, L, L, L, D, D, D, D},
            {D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D},
            {D, D, D, D, L, L, L, D, D, D, L, L, L, D, D, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, L, D, D, D, D, L, D, L, D, D, D, D, L, D, D},
            {D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D},
            {L, D, L, D, L, L, L, D, D, D, L, L, L, D, L, D, L},
            {D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D, D},
            {L, D, L, D, D, L, D, D, L, D, D, L, D, D, L, D, L},};

        LENGTH = initialBoard.length;
        p.setBoardLength(LENGTH);
        p.fillBoard(initialBoard);
    }

    private String getText() {
        boolean[][] board = p.getActualBoard();
        String boardStr = "\n";
        for (int i = 0; i != LENGTH; i++) {
            for (int j = 0; j != LENGTH; j++) {
                boardStr += board[i][j] ? CELL_LIVE_CHAR : CELL_DEAD_CHAR;
            }
            boardStr += "\n";
        }
        p.nextStep();
        return boardStr;
    }

    private AnimationTimer getTimer(final Text text) {
        return new AnimationTimer() {
            @Override
            public void handle(long l) {
                text.setText(getText());
                try {
                    Thread.sleep(tick);
                } catch (InterruptedException ex) {
                }
            }
        };
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Pruebas:
https://github.com/rodrigoSaladoAnaya/GameOfLife/tree/master/GameOfLife/test/game

PD: ahora que tenga tiempo, me lo voy a echar en Juby y lo pondré aquí diciendo que esta en Jruby. :P.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de rodrigo salado anaya

Upss

Aquí hay algo que no esta muy bien:

public boolean getCellHealth(boolean[] neighbours) {
        this.fillNeighbours(neighbours);
        int neigthbourdCount = this.getNeighboursCount();
        boolean cellHealth = this.getCellHealth();
        cellHealth = this.applyRules(neigthbourdCount, cellHealth);
        this.setCellHealt(cellHealth);
        return this.getCellHealth();
    }

Mejor así:

public boolean getCellHealth(boolean[] neighbours) {
        fillNeighbours(neighbours);
        return this.applyRules(getNeighboursCount(), getCellHealth());
    }

Hacerle caso a los patrones que emergen

Es verdad eso de ver como surgen los patrones, pero lo más importante es hacerles caso y refactorizar. Lo malo es que a veces por el cansancio, las prisas o que se yo, ignoramos esos patrones.

A mi me sirve ( y quizá a veces me ha causado más dolores de cabeza esto ) alinear todas mis columnas y paréntesis. Si, si ya sé, cae en lo maniático ( como cuando ves un tirante doblado y quieres irlo a componer ) pero en esta ocasión resultó.

Una forma para determinar un patrón es a simple vista... y con los ifs así como los tienes es difícil, así que lo primero que hice fue acomodarlos en columnas:


( No se ve bien el código así que lo pongo acá en un gist gist https://gist.github.com/3700549 )

Listo, lo siguiente es ver que valores son constantes, por ejemplo, en la segunda columna de la invocación a setNeighboursValues ( la que dice 0, length y x ) se puede leer como:

segundaColumna:
si   x == 0  entonces segundaColumna = 0
si   x == length  entonces segundaColumna = length
sino  segundaColumna = x

O sea que la segunda columna siempre es x Entonces, reemplazamos con x.

Lo mismo pasa con las y's. La 5ta columna siempre es y

Queda así ( además al tener todo por columnas lo puedo re-ordenar para agruparlos por los que se parezcan visualmente )

( gist: https://gist.github.com/3700589 )

Ya se van viendo los patrones no?... por cierto ahí hay un return sin condición que al moverlo pierde sentido, se la agregamos:

if (x > 0 && x < length && y > 0 && y < length){return setNeighboursValues(x - 1 , x , x + 1, y - 1, y , y + 1 ); }

Si le damos otra arreglada moviendolas condiciones podemos ver más claramente los patrones, por ejemplo que en la primera columna siempre es o lenght o x -1, la segunda siempre es x y la tercera siempre es o 0 o x+1 y ahí se ve también que lo mismo pasa para las y's

Entonces, lo que resta por hacer es determinar cuando toma un valor u otro. Podemos entonces traducirlo en 6 variables, de la a a la  f una por columna:

int a = x - 1;  
int b = x;
int c = x + 1;

int d = y - 1;
int e = y;
int f = y + 1;

// ajustes
if( x == 0      ) { a = length; }
if( x == length ) { c = 0;      }
if( y == 0      ) { d = length; }
if( y == length ) { y = 0;      }
return setNeighboursValues(a,b,c,d,e,f);

Dadaaaaaa, y adios if's ( casi ).

Ahora nos quedamos solo con cuatro, pero mmmhh como ya vimos que lo mismo que se hace para x se hace para la y y además es el mismo if, entonces podemos sacar otra función. Se ve que una es para la condición "antes" de n y otra es para la condición "después" de n, así que sacamos un método para cada uno.

private int before( int n ) {
    return n == 0 ? length : n - 1;
}
private int after( int n ) {
    return n == length ? 0 : n + 1;
}

Y con esto se puede hacer un inline de las variables y prescindir de ellas:

return setNeighboursValues(before(x), x, after(x), before(y), y, after(y) );

Ahora sí....dadaaaaaaaaaa.

Se redujo a solo dos if ( alguna lógica habría que tener no? ) y una linea muy legible de código.

Ahora que se ve bien, es claro, que pues siiii lo que está haciendo es tomar valores antes durante y despues de x y de y.

Entonces tu métodogetNeighboursByCell(int x, int y) quedaría así:

protected boolean[] getNeighboursByCell(int x, int y) {
    return setNeighboursValues(before(x), x, after(x), before(y), y, after(y) );
}

Hay que hacer algunos ajustes por aquello de length -1 pero creo que serán fáciles. Ahi me avisas si funciona porque jamás lo ejecuté :P

update: Si corrió, ahí va el pull request: https://github.com/OscarRyz/GameOfLife/commit/4721a180133eee490c99434698...

Imagen de rodrigo salado anaya

El poder de abstracción no me basta.

Que padre te quedo, muchas gracias. Aun que ya platicamos, no esta de más comentar que la parte de la visión humana juega un rol fundamental al momento de programar. No tengo a ni un conocido que tenga incapacidad visual y programe, pero en lo personal el poder de abstracción no me basta para poder llegar a lo que me muestras arriba.

Muchas gracias por la aportación.

Imagen de Sr. Negativo

El juego de la vida

Muy buen aporte.

Imagen de Sr. Negativo

Manual de git muy recomendable

Para los que no sabemos manejar muy bien Git.

http://www-cs-students.stanford.edu/~blynn/gitmagic/intl/es/index.html