4
\$\begingroup\$

Firstly, let me say that this code works, as posted. It fills a 10x10 grid with a random color (black/white) then runs a cellular automata simulation on the grid, starting or pausing when the button is pressed.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ConcurrentModificationException;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;


public class CA_DriverV2 extends JFrame{

    private static final Color white = Color.WHITE, black = Color.BLACK;

    private Board board;
    private JButton start_pause;

    public CA_DriverV2(){

        board = new Board();
        board.setBackground(white);

        start_pause = new JButton("Start");
        start_pause.addActionListener(board);

        this.add(board, BorderLayout.NORTH);
        this.add(start_pause, BorderLayout.SOUTH);
        //this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.pack();
        this.setVisible(true);

    }

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

    private class Board extends JPanel implements ActionListener{

        private final Dimension DEFAULT_SIZE = new Dimension(10, 10);
        private final int DEFAULT_CELL = 10, DEFAULT_INTERVAL = 100, DEFAULT_RATIO = 50;

        private Dimension board_size;
        private int cell_size, interval, fill_ratio;
        private boolean run;
        private Timer timer;

        private Color[][] grid;

        public Board(){
            board_size = DEFAULT_SIZE;
            cell_size = DEFAULT_CELL;
            interval = DEFAULT_INTERVAL;
            fill_ratio = DEFAULT_RATIO;
            run = false;

            //Initialize grid with random values
                //NOTE: Add JOptionPane for option to define fill rate and board size?
                //ALT: Have a resize(int h, int w) method to resize grid when needed.
                //ALT: Have refill(int r) method to restart with different fill ratio.
            grid = new Color[board_size.height + 1][board_size.width + 1];
            for (int h = 0; h < board_size.height; h++)
                for (int w = 0; w < board_size.width; w++){
                    int r = (int)(Math.random() * 100);
                    if (r >= fill_ratio)
                        grid[h][w] = black;
                    else grid[h][w] = white;
                }

            timer = new Timer(interval, this);
        }

        @Override
        public Dimension getPreferredSize(){
            return new Dimension(board_size.height * cell_size, board_size.width * cell_size);
        }

        @Override
        public void paintComponent(Graphics g){
            super.paintComponent(g);
            for (int h = 0; h < board_size.height; h++)
                for (int w = 0; w < board_size.width; w++){
                    try{
                        if (grid[h][w] == black)
                            g.setColor(black);
                        else g.setColor(white);
                        g.fillRect(h * cell_size, w * cell_size, cell_size, cell_size);
                    } catch (ConcurrentModificationException cme){}
                }
        }

        public void actionPerformed(ActionEvent e) {

            //Timer tick processing
            if (e.getSource().equals(timer)){
                repaint();
                Color[][] newGrid = new Color[board_size.height][board_size.width];
                for (int h = 0; h < board_size.height; h++)
                    for (int w = 0; w < board_size.width; w++) {
                        int surrounding = 0;
                        //Count black neighbors
                        for (int i = -1; i <= 1; i++)
                            for (int j = -1; j <= 1; j++)
                                if((h + i >= 0) && (h + i < board_size.height)
                                        && (w + j >= 0) && (w + j < board_size.width)
                                        && !(i == 0 && j == 0) && (grid[h + i][w + j] == black))
                                    surrounding++;

/*                      //Generate next iteration                                       */
                        if (grid[h][w] == black){
                            if (surrounding == 2 || surrounding == 3)
                                newGrid[h][w] = black;
                            else newGrid[h][w] = white;
                        }
                        else if(surrounding == 3)
                            newGrid[h][w] = black;
                    }
                for (int h = 0; h < board_size.height; h++){
                    for (int w = 0; w < board_size.width; w++){
                        grid[h][w] = newGrid[h][w];
//                      if (grid[h][w] == black)
//                          System.out.print("b ");
//                      else System.out.print("w ");
                    }
                    //System.out.println();
                }
                //System.out.println();

            }

            //Start-Pause button processing
            else if(e.getSource().equals(start_pause)){
                if(run){
                    timer.stop();
                    start_pause.setText("Start");
                }
                else {
                    timer.restart();
                    start_pause.setText("Pause");
                }
                run = !run;

            }
        }
    }
}

Any comments on the efficiency of the code would be appreciated. I want this code to be able to scale well both in size and iteration speed. I'm planning to add more features later, but I want to clean up the framework first.

\$\endgroup\$
  • 3
    \$\begingroup\$ Welcome to CR! Do you expect your code to work with a non-square grid? We can help you make your code better, but if a customizable grid is a feature and it's broken, that puts your question on the fence... I would fix the bug first, and get the code peer reviewed after :) \$\endgroup\$ – Mathieu Guindon Sep 30 '14 at 1:27
  • 2
    \$\begingroup\$ It's okay to call out that the code doesn't work in situation X, but it's off topic to ask how to make it work for situation X. Otherwise, this is an interesting question. I hope you don't mind, but I'm going to make an edit. \$\endgroup\$ – RubberDuck Sep 30 '14 at 2:02
  • \$\begingroup\$ I have found the bug, so let's clear that out of the way, and leave the code available for review..... You have, in two places, used boardsize.height instead of boardsize.width in the for-loops in the actionPerformed call. \$\endgroup\$ – rolfl Sep 30 '14 at 2:44
  • \$\begingroup\$ After you have fixed the bug, we can reopen the code for review. \$\endgroup\$ – rolfl Sep 30 '14 at 2:48
  • 1
    \$\begingroup\$ I have corrected the bug. This is not a typical thing to happen, and a meta discussion may be in order. \$\endgroup\$ – rolfl Sep 30 '14 at 3:30
2
\$\begingroup\$

When you are ready to worry about making the engine fast, read Michael Abrash. His Black Book has a chapter dedicated to fast life engines.

In your current implementation, you have your life engine tangled with your swing UI. That's probably a mistake; at a minimum, it will be a lot easier to check your engine for correctness if they are decoupled from the UI. For example, one easy way to test a new fast algorithm is to verify that it produces the exact same answers as a slow reliable approach. That's trivial to do with a unit test, if you have a direct way to read the state of the cells of the board.

But your basic arrangement to start should likely resemble: a representation of the game state, an engine that knows how to compute the next state from the current state, and broadcasts events describing the changes, and a view that knows how to represent the changes in the UI.

The view will need to know how to talk to Spring, but the engine doesn't care - as far as the engine is concerned, you could be drawing the results with ASCII characters.

private Color[][] grid;

That's your game state. I'm not too keen on using Color as the type, rather than boolean, or an enum of your own. Were you planning to do something clever if one of the cells turns red?

            Color[][] newGrid = new Color[board_size.height][board_size.width];
            for (int h = 0; h < board_size.height; h++)
                for (int w = 0; w < board_size.width; w++) {
                    int surrounding = 0;
                    //Count black neighbors
                    for (int i = -1; i <= 1; i++)
                        for (int j = -1; j <= 1; j++)
                            if((h + i >= 0) && (h + i < board_size.height)
                                    && (w + j >= 0) && (w + j < board_size.width)
                                    && !(i == 0 && j == 0) && (grid[h + i][w + j] == black))
                                surrounding++;

                  //Generate next iteration                                       
                    if (grid[h][w] == black){
                        if (surrounding == 2 || surrounding == 3)
                            newGrid[h][w] = black;
                        else newGrid[h][w] = white;
                    }
                    else if(surrounding == 3)
                        newGrid[h][w] = black;
                }
            for (int h = 0; h < board_size.height; h++){
                for (int w = 0; w < board_size.width; w++){
                    grid[h][w] = newGrid[h][w];
                }
            }
        }

That's your engine - it's really got two different pieces to it; a factory that creates a new state, and then a calculator that figures out what the state looks like. The only thing that is missing (because in your design, it is implicit), is a way to notify the rest of the application of what the changes are. It's completely fine to broadcast the entire grid as the "new state", but you will eventually discover faster alternatives.

        for (int h = 0; h < board_size.height; h++)
            for (int w = 0; w < board_size.width; w++){
                try{
                    if (grid[h][w] == black)
                        g.setColor(black);
                    else g.setColor(white);
                    g.fillRect(h * cell_size, w * cell_size, cell_size, cell_size);
                } catch (ConcurrentModificationException cme){}
            }

There's your view. He needs to know how to draw live and dead cells, but doesn't need to know anything about the rules that choose cells which are alive or dead.

Make separate classes for each of those, and wire them together.

Notes on style: your code here is difficult to read, because of some of the choices you are making. For readability among a large, respectful audience... make different choices.

private static final Color white = Color.WHITE, black = Color.BLACK;
  • One variable definition per line.
  • Static constants are normally spelled with UPPER_CASE names. Notice the example provided by java.awt.Color

Where possible, you should prefer to inject your dependencies.

public CA_DriverV2(){

    board = new Board();
    board.setBackground(white);

A better alternative would be to pass in a correctly initialized Board

public CA_DriverV2(Board board){
    this.board = board;

CA_DriverV2 is an opaque class name; much better to spell out CellularAutomataDriverV2. Better still, admit that it's ConwaysLifeDriver.

                    for (int i = -1; i <= 1; i++)
                        for (int j = -1; j <= 1; j++)
                            if((h + i >= 0) && (h + i < board_size.height)
                                    && (w + j >= 0) && (w + j < board_size.width)
                                    && !(i == 0 && j == 0) && (grid[h + i][w + j] == black))
                                surrounding++;

Oh my goodness - never drop the "optional" braces when there is any chance of confusion. Better still, never drop the braces -- we have plenty to spare.

\$\endgroup\$
  • \$\begingroup\$ + 100 for Oh my goodness - never drop the "optional" braces when there is any chance of confusion. Better still, never drop the braces -- we have plenty to spare \$\endgroup\$ – Heslacher Sep 30 '14 at 8:27
  • \$\begingroup\$ Game of Life just happened to be the ruleset I had implemented for the time being. I was planning on implementing a way to make the ruleset modifiable to some extent. I found a couple rather interesting simulations by throwing in random ruleset. \$\endgroup\$ – ViggyNash Sep 30 '14 at 19:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.