2
\$\begingroup\$

I've wrote a simulation of Langton's Ant in Java, and hope you have some suggestions to improve the code.

The class Control.java contains just the main:

import java.io.IOException;
public class Control {
    static boolean exit = false;
    public static void main(String[] args) throws InterruptedException, IOException {
        LangtonsAnt langtonsAnt = new LangtonsAnt(71, 71);
        Gui gui = new Gui(langtonsAnt);
        gui.setVisible(true);
        while(!exit) {
            gui.render();
            Thread.sleep(10);
        }
    }
}

The class Gui.java has the task to create and run the GUI:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class Gui extends JFrame {
    LangtonsAnt langtonsAnt;

    JPanel panel;
    JButton button;


    private final int BOX_DIM = 10;
    public Gui(LangtonsAnt langtonsAnt) {
        this.langtonsAnt = langtonsAnt;

        setSize(830, 740);
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        panel = new JPanel();
        JPanel subPanel = new JPanel();
        subPanel.setLayout(new java.awt.GridLayout(3, 1));
        panel.setPreferredSize(new Dimension(710, 710));

        button = new JButton("Start / Stop");
        button.addActionListener(
                new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        onButtonClick();
                    }
                });


        JButton button2 = new JButton("Faster");
        button2.addActionListener(
                new ActionListener() {

                    public void actionPerformed(ActionEvent e) {
                        langtonsAnt.faster();

                    }

                });
        JButton button3 = new JButton("Slower");
        button3.addActionListener(
                new ActionListener() {

                    public void actionPerformed(ActionEvent e) {
                        langtonsAnt.slower();

                    }

                });

        add(panel, BorderLayout.WEST);
        subPanel.add(button);
        subPanel.add(button2);
        subPanel.add(button3);
        add(subPanel, BorderLayout.EAST);
        setVisible(true);

    }

    private void onClose() {
        Control.exit = true;
    }

    private void onButtonClick() {
        isRunning = !isRunning;
    }

    private int countedFrames = 1;
    boolean isRunning = false;


    public void render() {
        countedFrames++;
        if(isRunning && countedFrames % langtonsAnt.speed == 0) {
            langtonsAnt.tick();
            countedFrames = 1;
        }

        Graphics graphic = panel.getGraphics();

        for (int i = 0; i < langtonsAnt.getWidth(); i++) {
            for (int j = 0; j < langtonsAnt.getHeight(); j++) {
                if(langtonsAnt.get(i, j) == 0) {
                    graphic.setColor(Color.WHITE);
                }
                else if(langtonsAnt.get(i, j) == 1) {
                    graphic.setColor(Color.BLACK);
                }
                else if(langtonsAnt.get(i, j) == 2) {
                    graphic.setColor(Color.RED);
                }
                else if(langtonsAnt.get(i, j) == 3) {
                    graphic.setColor(Color.GREEN);
                }
                graphic.fillRect(i * BOX_DIM, j * BOX_DIM, BOX_DIM, BOX_DIM);
            }
        }
    }

    public static void antDied() {
        JOptionPane.showMessageDialog(null,"Ant died.");
    }
}

And finally, the class LangtonsAnt.java is responsible for the logic:

public class LangtonsAnt {
    private int[][] world;
    private int width;
    private int height;
    public int speed = 10;

    public LangtonsAnt(int width, int height) {
        this.width = width;
        this.height = height;
        world = new int[width][height];
        //Field edge is getting filled up with '2'
        for(int i = 0; i < world.length; i++){
            for(int j = 0; j < world.length; j++){
                world[i][j] = 2;
            }
        }

        //Field gets filled up with '0' = white cells; '1' will be used for black cells; '3' for the ant
        for(int i = 1; i < world.length - 1; i++){
            for(int j = 1; j < world.length - 1; j++){
                world[i][j] = 0;
            }
        }

        world[35][35] = 3;
    }

    public void tick() {
        applyRules();
    }

    public int get(int x, int y) {
        return world[x][y];
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public void clear() {
        for(int i = 1; i < world.length - 1; i++){
            for(int j = 1; j < world.length - 1; j++){
                world[i][j] = 0;
            }
        }
    }

    public void faster() {
        if(speed - 5 > 0) {
            speed = speed - 5;
        }

    }

    public void slower() {
        speed = speed + 5;
    }

    private String color = "WHITE";
    private String direction = "DOWN";

    private void applyRules() {
        int length = world.length;

        int x = 0;
        int y = 0;
        for(int i = 0; i < length; i++) {
            for(int j = 0; j < length; j++) {
                if(world[i][j] == 3) {
                    x = i;
                    y = j;
                }
            }
        }

        if(color.equals("BLACK")) {
            if(direction.equals("UP")) {
                direction = "LEFT";
            }
            else if(direction.equals("DOWN")) {
                direction = "RIGHT";
            }
            else if(direction.equals("RIGHT")) {
                direction = "UP";
            }
            else if(direction.equals("LEFT")) {
                direction = "DOWN";
            }
        }
        else if(color.equals("WHITE")) {
            if(direction.equals("UP")) {
                direction = "RIGHT";
            }
            else if(direction.equals("DOWN")) {
                direction = "LEFT";
            }
            else if(direction.equals("RIGHT")) {
                direction = "DOWN";
            }
            else if(direction.equals("LEFT")) {
                direction = "UP";
            }
        }

        int xDirection = 0;
        int yDirection = 0;

        if(direction.equals("UP")) {
            xDirection = 0;
            yDirection = -1;
        }

        else if(direction.equals("DOWN")) {
            xDirection = 0;
            yDirection = 1;
        }

        else if(direction.equals("RIGHT")) {
            xDirection = 1;
            yDirection = 0;
        }

        else if(direction.equals("LEFT")) {
            xDirection = -1;
            yDirection = 0;
        }

        if(color.equals("WHITE")) {
            world[x][y] = 1;
        }
        else if(color.equals("BLACK")) {
            world[x][y] = 0;
        }

        if(x + xDirection < 69 && y + yDirection < 69) {
            if(world[x + xDirection][y + yDirection] == 0) {
                color = "WHITE";
            }
            else if(world[x + xDirection][y + yDirection] == 1) {
                color = "BLACK";
            }

            world[x + xDirection][y + yDirection] = 3;

        }

        else {
            Gui.antDied();
            world[x][y] = 1;
            Control.exit = true;
        }
    }
}
```
\$\endgroup\$
2
\$\begingroup\$

I reviewed LangtonsAnt.java only.

Observe Separation of concerns

The class violates this principle . The class is responsible for both applying Langton's Ant rules, and implementing the world as two dimension array. these are two separate topics that may have multiple implementations. the rules engine should be concerned with deciding where is the next position of the ant and possibly color change of one or more cells. the world can be implemented as array, some collection or something else (even a String is viable, as it have indices of its characters) and it knows how to represent state and movement, it knows to get current position of the ant (possibly multiple ants?), and perhaps other features (undo?)

Avoid magic numbers and String literals

instead of explaining the possible values a cell can have in comments, make constants:
public static final int WHITE_CELL = 0;
or better yet - if a variable can hold a finite predefined set of values - make it an enum. this is especially useful for the direction literals. this way you get the compiler to check for typos.

another added bonus of enum is that it supports conversion of String value from/to int (ordinal). so you do not need to ask on the String value when populating the matrix with int values.

Express the state machine in map

applying the rules of Langton's Ant is implemented as a set of if statements. this is error prone and cumbersome. a better way is to implement the rules as set of key-value pairs that can be put in a map. you can concatenate the key factors (color + direction) and get the desired direction. this way you can read the rules from file as String key value pairs and support different set of rules! (and have it in one RuleEngine class that is responsible for that only!)

Duplicated Code

The constructor fills the world with zeros. but this is exactly what's done in clear()

Fill array with zeros

while on the subject, Java 8 added setAll() to Arrays class. it works on one dimensional array, so it saves the inner loop. However, it so happens that 0 is the default value for uninitialized int, so declaring a new (one dimension) array gives you the desired result.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

The GUI

Usually, in the "view" side, you can decompose your frames in many components, this smart and dumb components pattern is a simple one that give a better structure to your code.

You can extract one class for the buttons and another for the grid. You can also create a subclass of JButton for all of your buttons and use the fancy lambdas notations for a concise syntax :

public Gui(LangtonsAnt langtonsAnt) {
    this.simulation = langtonsAnt;

    worldGrid = new WorldGrid(simulation);

    JPanel buttons = new VerticalStackPanel( // Your Jpanel with GridLayout
            new ActionButton("Start / Stop", this::onStartStop),
            new ActionButton("Faster", langtonsAnt::increaseSpeed),
            new ActionButton("Slower", langtonsAnt::decreaseSpeed)
    );

    add(worldGrid, BorderLayout.CENTER);
    add(buttons, BorderLayout.EAST);

    setSize(
            worldGrid.getWidth() + BUTTONS_WIDTH + ARBITRARY_SPACE,
            worldGrid.getHeight() + ARBITRARY_SPACE);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    // duplicated setVisible(true) removed, alrdeay called in your Control.
}

The core, LangtonsAnt

Your code contains some magic numbers, you can replace them by constants to reduce the cognitive load and improve the readability.

You have a speed field that is accessible from all classes. However it is subject to some rules. So it is better to encapsulate it under a getter so that other classes cannot change it, they have to use your method to increase and decrease it. It can also be a good improvement to rename your slower and faster method to decraseSpeed and increaseSpeed so that the relation between those two methods and the getSpeed accessor is clear.

In your system, the speed is part of the GUI so it may be better to move it to the GUI or remove it form the LangtonsAnt class to improve the separation of concerns. For the same goal you could also find a way to remove the static call to Gui.antDied() and Control.exit = true because it add two useless and non explicit dependencies.

It looks like there is a bit of repetition in your rules. The proposal of @sharon is a good idea. However it don't use the power of OOP, I believe it would be a good exercise because there are a lot of possibles implementations.

To have an idea, mine will have one class that represent the map, encapsulate the two dimensional array and provide a Cell abstraction. One Ant that hold the state of the system (current cell and direction) and produce movements that can be executed to mutate the map.

class WorldMap {
    private static final int WHITE_CELL = 1;
    private static final int BLACK_CELL = 0;

    private final int[][] map;

    // ..

    public Cell getCellAt(int x, int y)

    public void execute(Movement movement)

    class Cell {
        final int x, y;

        private void toggle() {
            world.map[y][x] ^= 1;
        }

        public boolean isWhite() {
            return get() == WHITE_CELL;
        }

        public Direction change(Direction direction) {
            return isWhite()
                ?direction.turnLeft()
                :direction.turnRight();
        }

        // ...
    } 
}

class Ant {

    private Direction direction = new Direction(Direction.UP);
    private Cell position;

    // ...

    class Direction {
        private static final int ROTATION = 90;
        private static final int UP = 0;
        private static final int RIGHT = UP + ROTATION;
        private static final int DOWN = RIGHT + ROTATION;
        private static final int LEFT = DOWN + ROTATION;
        private final int angle;

        Direction(int angle) {
            this.angle = angle;
        }

        public Direction turnRight() {
            return new Direction((angle + ROTATION) % 360);
        }
    }
} 
| improve this answer | |
\$\endgroup\$

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.