Search code examples
javaswingarraylistjframeconways-game-of-life

Attempt at Replicating Conway's Game of Life does not expand or work properly


If you don't know what Conway's game of life is check this, it is a zero player game, and a cellular automation. https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life

I need help debugging the following problems in my code. If necessary, I can edit this question to include older versions of my program, which are not intended to expand.

I will also accept tips to improve my program, including (but not limited to) time efficiency, (off topic however, as it is not the main question) and incorrect use of existing methods. (My paint() method)

Problems:

Examine Glider A in image 1. (Expected behavior) The whole point of the program was to make the cells shrink in the JFrame and add another row or column of dead cells to the ArrayList<ArrayList<Boolean>> grid. This glider, unfortunately does not cause this to happen. Instead, the result (Unexpected behavior) was that it simply "flattened into a square" as shown in image 2 (the square marked with an A).

Examine Glider B in image 1. (Expected behavior) Seeing the results Glider A in image 1 made me think that glider B would end the same. However, this is not remotely true. Look at glider B in image 2. It, for some reason, does not even reach the border. (Unexpected)

What this program is for and meant to do: Conway's game of life is on an infinite 2D plane, so I wanted to replicate this. My other programs used a fixed array size, but this used nested ArrayLists to expand, and truly be infinite (until the memory limit comes)

Unfortunately, this expansion program simply fails to expand the border. (The border is actually smaller than it looks. Basically, (pretending my ArrayList> grid is an array) grid[0][0 to grid.length-1] inclusive, grid[grid.length-1][0 to grid.length-1] inclusive, and grid[1 to grid.length-1][0] inclusive, and grid[1 to grid.length-1][grid.length-1] inclusive.) Instead of doing the specified steps to make room, glider A in image 1 flattened against the border. (The border is beneath the grid of cells, where grey lines are not present)

Image 1 enter image description here Image 2 enter image description here

These images correspond with each other; glider A from image 1 Heading produces the square in image 2, also marked as A. If you see any words describing "line 74," pointing toward "line 74" or words like "UP" (without any other meaning) in comments in the code please tell me, so I can remove them. That was a separate, already fixed bug.

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JFrame;
import javax.swing.Timer;
public class Expansion {
    static int delay = 50;
    static SquarePaint sp = new SquarePaint();
    static int K = 75; // An integer used below to create the starting setup for the grid.
    static{
        // 001 101 011
        boolean[][] array = new boolean [300][200];
        //      array[30][30]
        array[K][K+2] = true;
        array[K+1][K] = true;
        array[K+1][K+2] = true;
        array[K+2][K+1] = true;
        array[K+2][K+2] = true;
        sp.define(array); // Uses array to make first setup
    }
    public static void main (String[] args){
        // It is possible to put this information in a constructor instead
        JFrame j = new JFrame();
        j.add(sp); // Must add a custom paint component overriding the paint component, 
        //a class could and will do this
        j.setVisible(true);
        j.setSize(2000, 1000);
        j.setResizable(false);
        j.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public static class SquarePaint extends Component{
        static boolean VALUE = false;
        public static final int screenX = 1000, screenY = 500;
        static int cellSize = 5;
        int cellsX = screenX / cellSize, cellsY = screenY / cellSize, frames = 0;

        ArrayList<ArrayList<Boolean>> grid = new ArrayList<ArrayList<Boolean>>(); ////
        Timer timer = new Timer(delay, new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent A) {
                //UPDATE FUNCTION
                frames++;
                boolean [][] oldGrid = new boolean[cellsX][cellsY];
                for(int i=0; i<cellsX && i<grid.get(1).size(); i++){
                    for(int j=0; j<cellsY && j<grid.get(1).size(); j++){
                        oldGrid[i][j] = grid.get(i).get(j); // <<Line 74>><<Line 74>><<Line 74>>

                    }
                }
                /* These are notes
                 * How would I:
                 * Expand LinkedLists, for example
                         (Grid in a picture, 0 = variable, + = new cell)
                            0 0 0 +
                            0 0 0 +
                            0 0 0 +
                            0 0 0 +
                 * Adjust existing variables (and poorly coded methods if they exist) to suit this new change
                 */
                for(int i=1; i<cellsX-2; i++) {
                    for(int j=1; j<cellsY-2; j++) { // TODO
//                      System.out.println(i + " " + j);
                        int nearbyCells = nearbyCells(oldGrid, i, j);
                        /////////////////////////////////////////////
                        if(oldGrid[i][j]){ 
                            // If the cell is alive 
                            // and is beside the no interact border, expand it by 1
                            // (Basically, to prevent ArrayOutOfBoundsException s, 
                            // (the ArrayList equivalent) only cells from 1 to cellsX-1(-1)/cellsY-1(-1) 
                            // are counted,)
                            // meaning that a whole row/column of cells is ignored and is not interacted with,
                            // until the grid expands.
                            ArrayList <Boolean> l = new ArrayList<Boolean>();
                            for(int k=0; k<cellsX; k++){
                                l.add(false);
                            }
                            if(i <= 1){ // You may notice that the above comments are wrong.
                                // This is because I thought this might be a small chance for it to succeed
                                // while I was frustrated this was not working.
                                grid.add(0, l);
                                cellSize = screenX / (cellSize + 1);
                                cellsX++;
                                VALUE = true;
                                repaint();
                                System.out.println("i<=5");
                            }
                            if(i >= cellsX-1){
                                grid.add(l);
                                cellSize = screenX / (cellSize + 1);
                                cellsX++;
                                VALUE = true;
                                repaint();
                                System.out.println("i>=cellsX");
                            }
                            if(j <= 1) {
                                for(int k=0; k<cellsY; k++){
                                    grid.get(k).add(false);
                                }
                                cellSize = screenY / (cellSize + 1);
                                cellsY++;
                                VALUE = true;
                                System.out.println("j<=5");
                                repaint();
                            }
                            if(j >= cellsY-1) {
                                for(int k=0; k<cellsY; k++){
                                    grid.get(k);
                                }
                                cellSize = screenY / (cellSize + 1);
                                cellsY++;
                                VALUE = true;
                                System.out.println("j>cellsY");
                                repaint();
                            }
                            //corner cases aren't a problem, it'll add to both sides
                            //  && (i == 1 || i == cellsX-1 || j == 1 || j == cellsY-1)
                            /* i.e.
                             * 0 = dead cells
                             * 1 = annoying cells in the corner
                             * + = new cells
                             * First, it'll do this
                             * + + +
                             * 0 1 1
                             * 0 1 1 // Note that the square is a "still life form" meaning that it will
                             * 0 0 0 // simply sit there and do nothing for infinite generations if 
                             *      // untouched
                             * But the if statements above will return true more than once, so
                             * 0 0 0 +
                             * 0 1 1 +
                             * 0 1 1 +
                             * 0 0 0 +
                             */
                        }
                        /////////////////////////////////////////////
                        if (oldGrid[i][j] && !(nearbyCells == 2 || nearbyCells == 3)){ 
                            // if it is alive, sustain rules
                            grid.get(i).set(j, false);
                        }
                        else if (!oldGrid[i][j] && nearbyCells == 3){ // if it is dead, birth rules
                            grid.get(i).set(j, true);
                        }
                    }
                }
                repaint(); // never erase, note that in bigger applications I assume that they 
                // draw to pictures that get slapped onto the screen so the paint function doesn't 
                // get called 9999 times and slow the fps down

            }
        });
        int nearbyCells(boolean[][] oldGrid, int i, int j) { // A method that calculates how many cells are
            //alive near it, i.e.
            /* 0 = Dead
             * 1 = Alive
             * "+" = center
             * 1 0 0
             * 0 + 1
             * 0 0 1
             * If you called nearbyCells on the center in the example, then it would return 3.
             */
            int nearbyCells = 0;

            if(oldGrid[i+1][j+1]) nearbyCells++;
            if(oldGrid[i+1][j]) nearbyCells++;
            if(oldGrid[i+1][j-1]) nearbyCells++;

            if(oldGrid[i][j+1]) nearbyCells++;
            if(oldGrid[i][j-1]) nearbyCells++; // i, j is where the to-be-changed cell is, skip it

            if(oldGrid[i-1][j+1]) nearbyCells++;
            if(oldGrid[i-1][j]) nearbyCells++;
            if(oldGrid[i-1][j-1]) nearbyCells++;
            return nearbyCells;
        }
        public SquarePaint(boolean[][] grid){
            define(grid);
            timer.start();
        }
        public SquarePaint(){
            timer.start();
        }
        public void define (boolean[][] grid){ // define cannot be called twice, not intended
            for(int i=0; i<cellsX; i++){
                ArrayList<Boolean> l = new ArrayList<Boolean>();
                for(int j=0; j<cellsY; j++){
                    l.add(grid[i][j]);
                }
                this.grid.add(l);
            }
        }
        @Override
        public void paint(Graphics g) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, screenX, screenY); // Warning: Color must be set 
            // each time if the paint command is ever to be called again


            for(int i=0; i<cellsX-1; i++){
                for(int j=0; j<cellsY-1; j++){
                    try{
                        if(grid.get(i).get(j)){
                            g.setColor(Color.WHITE);
                            g.fillRect(i * cellSize, j * cellSize, cellSize, cellSize); 
                        } // Basically, 2 for loops
                    }// Nested for loops draw each cell on the potentially expanding ArrayList "grid" of cells

                    catch(java.lang.IndexOutOfBoundsException e){
                        System.out.println(i + " " + j);
                        e.getStackTrace();
                    }
                    g.setColor(Color.GRAY);
                    g.drawRect(i * cellSize, j * cellSize, cellSize, cellSize);
                }
            }


        }

    }

}

Solution

  • I have no idea what the logic in the middle of your application is supposed to be doing. Shouldn't be that complicated. This runs the simulation in another thread and so perhaps helps address performance.

    public class GameOfLife extends JFrame {
        private static final long serialVersionUID = 1L;
        private Thread thread;
        private SimulationThread simulationThread;
    
        public GameOfLife() {
            super("GameOfLife");
        }
    
    
        class MyPanel extends JPanel {
            private static final long serialVersionUID = 1L;
    
            public MyPanel() {
                setBorder(BorderFactory.createLineBorder(Color.black));
            }
    
            public Dimension getPreferredSize() {
                return new Dimension(1000, 1000);
            }
    
            public void paintComponent(Graphics g) {
                super.paintComponent(g);       
                Graphics2D g2d = (Graphics2D)  g;
                simulationThread.drawScreenItems(g2d);
            }
        }
        /**
         * Create the GUI and show it. For thread safety, this method should be invoked
         * from the event-dispatching thread.
         */
        public void createAndShowGUI() {
            // Schedule a job for the event-dispatching thread:
            // creating and showing this application's GUI.
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            MyPanel myPanel = new MyPanel();
            add(myPanel);
    
            // Display the window.
            pack();
            setLocationRelativeTo(null);
            setVisible(true);
            // Create and set up the window.
    
            // create thread with genetic reproduction callback code.
            simulationThread = new SimulationThread(myPanel);
            thread = new Thread(simulationThread);
            thread.start();
        }
    
        public static void main(String[] args) {
            final GameOfLife swingGa = new GameOfLife();
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    swingGa.createAndShowGUI();
                }
            });
        }
    }
    
    public class SimulationThread implements Runnable {
        private MyPanel myPanel;
        public boolean run = true;
        static final int GAME_SIZE = 200;
    
        boolean[][] cells;
        boolean[][] newCells;
    
        public SimulationThread(MyPanel myPanel) {
            this.myPanel = myPanel;
            cells = new boolean[GAME_SIZE][GAME_SIZE];
            for (int i = 1; i < cells.length - 1; i++) {
                for (int j = 1; j < cells[0].length - 1; j++) {
                    cells[i][j] = Math.random() > 0.5;
                }
            }
            newCells = new boolean[GAME_SIZE][GAME_SIZE];
        }
    
        @Override
        public void run() {
            while (run) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                // Iterate through the array, follow game of life rules
                for (int i = 1; i < cells.length - 1; i++) {
                    for (int j = 1; j < cells[0].length - 1; j++) {
                        int surrounding = 0;
                        if (cells[i - 1][j - 1]) surrounding++;
                        if (cells[i - 1][j]) surrounding++;
                        if (cells[i - 1][j + 1]) surrounding++;
                        if (cells[i][j - 1]) surrounding++;
                        if (cells[i][j + 1]) surrounding++;
                        if (cells[i + 1][j - 1]) surrounding++;
                        if (cells[i + 1][j]) surrounding++;
                        if (cells[i + 1][j + 1]) surrounding++;
                        newCells[i][j] = false;
                        if (cells[i][j]) {
                            // Cell is alive, Can the cell live? (2-3)
                            if ((surrounding == 2) || (surrounding == 3)) {
                                newCells[i][j] = true;
                            }
                        } else {
                            // Cell is dead, will the cell be given birth? (3)
                            if (surrounding == 3) {
                                newCells[i][j] = true;
                            }
                        }
                    }
                }
                synchronized(cells) {
                    for (int i = 1; i < cells.length - 1; i++) {
                        for (int j = 1; j < cells[0].length - 1; j++) {
                            cells[i][j] = newCells[i][j];
                        }
                    }
                }
                myPanel.repaint();
            }
    
        }
    
        public void drawScreenItems(Graphics2D g2d) {
            synchronized(cells) {
                for (int i = 1; i < cells.length - 1; i++) {
                    for (int j = 1; j < cells[0].length - 1; j++) {
                        if (cells[i][j])
                            g2d.fillRect(i * (1000 / GAME_SIZE), j * (1000 / GAME_SIZE), 1000 / GAME_SIZE, 1000 / GAME_SIZE);
                    }
                }
            }
        }
    }