Компьютерная графика → Рисуем простой клеточной автомат на Java

Привет, всем! В этой статье покажу как используя ООП, написать простой клеточный автомат на языке Java. 

Для примера возьмем клеточный автомат - тьюрмит "Лабиринт". Как вы знаете, тьюрмит - это объект, который двигается по полю по заданным правилам и может изменять цвет клетки, где он находится. Тьюрмит имеет условное состояние и направление движения. 

Для лабиринта правила такие:

 

A   0   1   -1   A 
A   1   2   -1   A
A   2   3   -1 A
A   3   4   -1 A
A   4   5   -1 A
A   5   6   1 B
B   0   1   1 A
B   5   6   -1 B
B   6   7   -1 B
B   7   8   -1 B
B   8   9   -1 B
B   9   10   -1 B
B   10   11   -1 B
B   11   12   -1 B
B   12   13   -1 B
B   13   14   -1 B
B   14   15   -1 B
B   15   0   -1 B

Как видно из правил у тьюрмита есть два возможных состояния: A, B. Есть 16 разных цветов клеток поля.

Напомню как читать правила: Первые два значения  - это условия: текущее состояние тьюрмита и цвет клетки где он находится. Третье значение показывает на какой цвет нужно поменять текущую клетку. Четвертое число означает куда должен повернуть тьюрмит: -1 налево, 0 - не поварачивать, 1 направо. И последнее значение означает в какое состояние переходит тьюрмит после этого шага.  

За каждый шаг тьюрмит ищет правило по первым двум значениям, и применяет его. 

Итак приступим

Сначала создадим простое окно с одной панелью, на которой мы будем рисовать. 

файл LabyrinthCellAutomata.java:

import javax.swing.*;

public class LabyrinthCellAutomata {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Лабиринт");
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

При запуске эта программа покажет просто окно. В Java, чтобы рисовать, нужно создать свой класс, который будет наследовать какой-нибудь Компонент.

Теория:

Компоненты в Java это визуальные элементы, которые можно отобразить на экране: JFrame, JPanel, JTextField, JButton .... Все эти классы являются наследниками класса Component. И у каждого из них есть метод paint(), который они унаследовали от класса Component. Java вызывает метод paint() чтобы нарисовать на экране этот элемент. Например метод paint() класса JButton рисует на экране кнопку. 

 

В нашем случае мы унаследуем класс JPanel и создадим класс CellAutomataPanel - это будет нашей собсвенной панелью. И в этом классе нам нужно переопределить метод paint() чтобы мы могли указать, как должна отрисовываться наша панель. Мы там будем рисовать наш клеточный автомат.

Файл CellAutomataPanel.java:

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

public class CellAutomataPanel extends JPanel {
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        
        // Здесь мы будем рисовать
    }
}

Теперь создадим объект этого класса и подключим его в наше созданное вначале окно:

Файл LabyrinthCellAutomata.java:

import javax.swing.*;

public class LabyrinthCellAutomata {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Лабиринт");
        frame.setSize(400, 400);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        CellAutomataPanel myPanel = new CellAutomataPanel();

        frame.setContentPane(myPanel);
        frame.setVisible(true);
    }
}

Мы добавили нашу панель в окно как основную панель. Если мы запустим сейчас, то также будет пустое окно. Поэтому чтобы убедиться что наша панель там есть и работает. Мы нарисуем в нашей панели несколько маленьких квадратиков. 

Теория:

Метод paint(Graphics g), как видно в классе CellAutomataPanel, принимает один параметр g класса Graphics. Через этот объект рисуется содержимое любого компонента, в том числе панели.  

 

Чтобы нарисовать квадратики напишите внутри метода paint() следуюшие строчки:

g.setColor(Color.BLUE);
g.fillRect(200, 200, 10, 10);

g.setColor(Color.RED);
g.fillRect(250, 200, 10, 10);

После этого запустив программу увидите два квадратика. 

Поле клеточного автомата

У клеточного автомата есть поле, где он работает. В нашем случае поле пусть будет размером 40х40 клеток. Это поле будет меняться постоянно и будем хранить состоние поля в двумерном массиве. При каждом шаге тьюрмит будет менять поле, и мы будем перерисовывать экран в соответсвие с полем. 

Каждая клетка поля имеет только цвет. В нашем автомате всего 16 цветов: от 0 до 15. Вначале все клетки закрашены цветом 0. Поэтому двумерный массив представляющий поле будет типа int. 

int[][] field = new int[40][40];

В Java по умолчанию такой массив будет заполнен нулями. Поле будет свойством класса CellAutomataPanel, поэтому объявим свойство field внутри этого класса:

public class CellAutomataPanel extends JPanel {
    public int[][] field = new int[40][40];
   
    ...

Поле у нас есть, пусть пока пустое, мы должны написать метод, который будет рисовать это поле на экран. Для этого создаем метод drawField() в классе CellAutomataPanel, который будет рисовать поле. Каждая клетка нашего поля будет размера 10х10 пикселей при рисовании.

Чтобы нарисовать поле на экран, мы сделаем двойной цикл по ширине и по высоте поля и нарисуем каждую клетку отдельно. У нас будет массив из 16 цветов от 0 до 15 и еще метод который будет возвращать цвет по номеру его. 

Добавим два метода в класс CellAutomataPanel:

private void drawField(Graphics g) {
    for(int x=0; x<40; x++){
        for(int y=0; y<40; y++){

            int colorNumber = field[x][y];              // получаем номер цвета клетки поля по x,y
            Color color = colorWithNumber(colorNumber); // получаем цвет по номеру цвета
            g.setColor(color);                          // устанавливаем цвет для рисования

            g.fillRect(x*10, y*10, 10, 10);             // рисуем квадрат
        }
    }
}

private Color colorWithNumber(int number){
    Color[] colors = new Color[]{
            Color.BLACK, Color.RED, Color.GREEN, Color.BLUE,
            Color.GRAY, Color.YELLOW, Color.ORANGE, Color.PINK,
            Color.CYAN, Color.MAGENTA, new Color(52, 99, 255), new Color(85, 159, 255),
            new Color(36, 130, 100), new Color(130, 0, 255), new Color(196, 255, 101), new Color(255, 116, 153),
    };

    return colors[number];
}

Так как рисование происходит внутри метода paint(), мы должны внутри метода paint() вызвать метод drawField() чтобы нарисовать панель. Перед этим уберем из метода paint() рисование двух квадратиков:

@Override
public void paint(Graphics g) {
    super.paint(g);

    drawField(g);
}

Итак, у нас есть поле и мы умеем его рисовать. Давайте попробуем запустить таймер, и каждую секунду менять в поле что-то и перерисовывть его. Для этого откройте класс LabyrinthCellAutomata и добавить туда метод:

private static void startTimer(CellAutomataPanel myPanel) {
    Timer timer = new Timer(500, new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            Random random = new Random();
            myPanel.field[random.nextInt(40)][random.nextInt(40)] = random.nextInt(16);
            myPanel.repaint();
        }
    });

    timer.start();
}

Мы тут запускаем таймер с интервалом 500 мс и каждые 500 мс случайным цветом от 0 до 15 заполняем случайную клетку поля. Затем перерисовывем панель методом repaint().

Теперь этот метод надо вызвать в конце метода main():

public static void main(String[] args) {
    ...
    frame.setVisible(true);

    startTimer(myPanel);
}

Попробуйте запустить программу и посмотрите как случайные клетка закрашиваются в разные цвета. Само поле вначале черного цвета, потому что цвет 0 у нас черный. 

Тьюрмиты

Осталось теперь одного тьюрмита запустить в наше поле. Для этого мы создадим класс Turmit в котором будут такие свойства:
- state - текущее состояние тьюрмита. В начале состояние "A"
- direction - текущее направление тьюрмита . В начале "up" (вверх)
- x, y - текущие координаты тьюрмита. В начале будет середина поля.

Файл Turmit.java:

public class Turmit {
    public String state = "A";
    public String direction = "up";
    public int x = 20;
    public int y = 20;
}

У класса Тьюрмита будет метод nextStep(), который будет двигать тьюрмит на один шаг. В этом методе мы сначала получим цвет клетки, где находится тьюрмит, и на основе текущего состояния и цвета, определим что дальше делать по правилам нашего клеточного автомата "Лабиринт". 

У нас 17 правил для Лабиринта, и мы должны выбрать нужное правило по текущему состоянию тьюрмита и цвету клетки.   Мы будем использовать структуру HashMap чтобы хранить правила по ключам.

Теория:

HashMap - это ассоциативный массив. Если вкратце, то ассоциативный массив это структура данных которая хранит пары ключ-значения. В Hashmap мы можем быстро найти значение по его ключу. Как раз для наших правил. 

В HashMap все ключи должны быть одного типа, в значения другого типа. Например если мы хотим хранить Столицы стран по именам стран, то HashMap будет таким: HashMap<String, String>. потому что и ключ и значение типа String.

Так как ключом HashMap может быть только одно значение одного типа, но у нас как ключ будет использоваться состояние и цвет, мы будет объединять состояние и цвет в одну строку и будем ее использовать как ключ. Например: "A0", "A3", "B0". А значением а HashMap у нас будет три значения: новый цвет, поворот и новой состояние. Для этих трех мы создадим новый класс Rule, который будет объединять в себя все эти три.

Теперь класс Turmit выглядит так: 

import java.util.HashMap;

public class Turmit {
    public String state = "A";
    public String direction = "up";
    public int x = 20;
    public int y = 20;

    public void nextStep(int[][] field){
        int color = field[x][y];     // получем цвет клетки где находится тьюрмит (x, y)
        Rule rule = findRule(state, color);
    }

    public Rule findRule(String state, int color){
        HashMap<String, Rule> rules = new HashMap<>();

        rules.put("A0", new Rule(1, -1, "A"));
        rules.put("A1", new Rule(2, -1, "A"));
        rules.put("A2", new Rule(3, -1, "A"));
        rules.put("A3", new Rule(4, -1, "A"));
        rules.put("A4", new Rule(5, -1, "A"));
        rules.put("A5", new Rule(6, 1, "B"));
        rules.put("B0", new Rule(1, 1, "A"));
        rules.put("B5", new Rule(6, -1, "B"));
        rules.put("B6", new Rule(7, -1, "B"));
        rules.put("B7", new Rule(8, -1, "B"));
        rules.put("B8", new Rule(9, -1, "B"));
        rules.put("B9", new Rule(10, -1, "B"));
        rules.put("B10", new Rule(11, -1, "B"));
        rules.put("B11", new Rule(12, -1, "B"));
        rules.put("B12", new Rule(13, -1, "B"));
        rules.put("B13", new Rule(14, -1, "B"));
        rules.put("B14", new Rule(15, -1, "B"));
        rules.put("B15", new Rule(0, -1, "B"));

        return rules.get(state+color);
    }

    class Rule{
        public int nextColor;
        public int turn;
        public String nextState;

        public Rule(int color, int turn, String state){
            this.nextColor = color;
            this.turn = turn;
            this.nextState = state;
        }
    }
}

метод findRule находит нужно правила по состоянию и цвету. 

Как видно выше, метод nextStep принимает параметр field - это наше поле. При вызове метода nextStep() мы будем передавать поле. После того как мы выбрали правило, нужно его применить. Дополним метод nextStep() следующим кодом:

field[x][y] = rule.nextColor;   // красим клетку в новый цвет
state = rule.nextState;         // меняем состояния тьюрмита на новый

if(rule.turn == -1){                     // Если нужно повернуть налево
    if(direction.equals("up")){        // Если текущее направление вверх
        direction = "left";           // то меняем на лево
    } else if(direction.equals("left")){  // Если текущее направление  налево
        direction = "down";              // то меняем вниз
    } else if(direction.equals("down")){  // и т.д.
        direction = "right";
    } else if(direction.equals("right")){
        direction = "up";
    }
} else if(rule.turn == 1){             // Если нужно повернуть направо
    if(direction.equals("up")){
        direction = "right";
    } else if(direction.equals("left")){
        direction = "up";
    } else if(direction.equals("down")){
        direction = "left";
    } else if(direction.equals("right")){
        direction = "down";
    }
}

// После того как повернули тьюрмит, двигаем его на одну клетка по направлению

if(direction.equals("up")){
    y -= 1;
} else if(direction.equals("down")){
    y += 1;
} else if(direction.equals("left")){
    x -= 1;
} else  if(direction.equals("right")){
    x += 1;
}

Теперь наш тьюрмит готов к жизни и его пора расположить в панели, чтобы он начал жить. 

В классе CellAutomataPanel добавим еще одно свойство рядом с field:

public class CellAutomataPanel extends JPanel {
    public int[][] field = new int[40][40];
    public Turmit turmit = new Turmit();

это тьюрмит. 

И теперь мы по таймеру будем двигать тьюрмита на один шаг. Для этого для нашей панели тоже создаим метод nextStep():

public void nextStep(){
    turmit.nextStep(field);
    this.repaint();
}

Этот метод двигает тьюрмита и перерисовывает панель. Нужно изменить таймер. Откроем класс LabyrinthCellAutomata и метод startTimer поменяем на этот:

private static void startTimer(CellAutomataPanel myPanel) {
    Timer timer = new Timer(10, new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            myPanel.nextStep();
        }
    });

    timer.start();
}

Теперь таймер запускается с интервалом в 10 мс и каждый раз вызывает метод nextStep() нашей панели. 

Запустите программу и узрите двигающийся Тьюрмит! 

Полный код можете посмотреть здесь: https://gist.github.com/MasterAlish/54d8af8e4620b721e470b8513e218722

1371 0
Alisher Alikulov