«Мир проводов» в D

В природе существует великое множество различных динамических систем, поведение которых очень сложно. Но, как известно из нелинейной динамики, сложное и порой даже хаотическое поведение системы может получаться из самых простых и элементарных законов. С наглядными иллюстрациями этого факта мы уже несколько раз знакомились, например, аттрактор Лоренца: когда набор простых линейных преобразований приводит к тому, что движение точки внутри фазового пространства становится непредсказуемым при малейшем изменении любой из координат.

Недавно я вспомнил про несколько иной способ получения самых разнообразных форм поведения, настолько живой и наглядный, что было бы слишком опрометчиво не попробовать
реализовать его на D.
Способ, про реализацию которого я поведаю ниже, называется «клеточный автомат». По названию, несложно догадаться, что программа, которая работает с клеточным автоматом, делит некую плоскость (или в более широком смысле, пространство), на набор клеток. Условимся, что под словом «клетка», будем понимать нечто, что наиболее близко математическому понятию «ячейка плоскости», т.е. то, что получается при разбиении плоскости вертикальными и горизонтальными прямыми, идущими с некоторыми заданными шагами. Назовем шаг между вертикальными линиями длина клетки, а между горизонтальными – ее шириной. Запомните этот момент, он нам еще встретится.

Как было упомянуто ранее, плоскость разделена на клетки. Помимо этого, в клеточном автомате неявно присутствует и такая величина, как время и оно также подвергнуто делению. Исходя из этого, принято выделять своеобразный квант времени, т.е. наименьший промежуток во времени в течение которого, с каждой из клеток могут происходить изменения. Такой квант времени, играющий роль чего-то напоминающего секунду (или другой микроскопический отрезок времени), принято называть «поколение» или «тик».

Подведем небольшой итог: у нас есть пространство, разбитое на равные клетки, с определенной длиной и шириной; а также у нас есть время, единицей измерения которого является поколение.
Но наличие только пространства и времени не делает систему динамической, необходим набор законов, которые изменяют клетки. Назовем этот набор законов — «правила перехода» и будем считать, что на протяжении одного поколения к каждой клетке применяется только одно правило перехода из набора. Исходя из этого требования, выводится негласное правило о том, что в пространстве существует больше одного вида клеток, т.е. для правил перехода клетки имеют разную ценность (разный номинал или вид).
Таким образом, получаем клеточный автомат — интересную модель, которая изменяется во времени по весьма несложным правилам.
Клеточных автоматов очень много, существуют целые десятки или даже сотни, однако, большинство из них не так интересно и не столь наглядно, как например клеточный автомат под названием WireWorld.

WireWorld – это простой мир, созданный для симуляции и испытания электронных и логических схем. Английское название этого автомата указывает на то, что одним из объектов этого мира является провод (wire, англ.) или «проводник». Вторым объектом этого мира является электрон, который может двигаться только по проводам. Но электрон представлен в WireWorld не совсем обычным образом: в этом мире электрон считается частицей, у которой есть «голова» и «хвост», служащие для указания направления, в котором будет двигаться частица (электрон движется головой вперед). Также в WireWorld имеются пустые клетки.

Правила перехода в WireWorld достаточно просты, однако, одно из этих правил требует введения понятия «окрестность клетки». Окрестностью клетки мы назовем клетки, которые находятся на расстоянии одного хода короля от текущей клетки (т.е. в окрестность клетки войдут: одна клетка слева, одна клетка справа, одна клетка сверху, одна клетка снизу, одна клетка сверху слева, одна клетка сверху справа, одна клетка снизу слева и одна клетка снизу справа от исходной). Тогда правила перехода могут быть сформулированы следующим образом:

• пустая клетка переходит в пустую клетку;
• голова электрона переходит в хвост электрона;
• хвост электрона переходит в проводник;
• проводник переходит в голову электрона, если в его окрестности есть ровно 1 или 2 головы электрона.

Кроме того, предполагается, что пространство свернуто в сферу или тор, т.е. соседом крайней левой клетки становится крайняя правая клетка (то же самое верно в отношении крайней верхней и крайней нижней клетки).

Для описания элементов мира WireWorld воспользуемся перечислениями с типом byte, поскольку набор значений, которые мы будем представлять крайне узок:

// элементы мира WireWorld
final enum Element : byte 
{
	Empty,        // пустое поле
	Head,         // голова электрона
	Tail,         // хвост электрона
	Conductor     // проводник
}

Пространство в WireWorld можно описать с помощью обычного двумерного массива, содержащего элементы Element, но более практичным способом будет инкапсуляция этого массива в самостоятельный класс. Также, работа с массивами фиксированного размера более проста и требует меньше усилий, поэтому можно параметризовать класс двумя шаблонными параметрами, задающими количество клеток по длине и количество их по ширине.

Вот код класса WireWorld:

// мир WireWorld
class WireWorld(size_t WORLD_WIDTH, size_t WORLD_HEIGHT)
{
	private
	{
		// мир
		byte[WORLD_HEIGHT][WORLD_WIDTH] world;
		// копия мира
		byte[WORLD_HEIGHT][WORLD_WIDTH] reserved;

		// резервное копирование мира
		void backupWorld()
		{
			for (int i = 0; i < WORLD_WIDTH; i++)
			{
				for (int j = 0; j < WORLD_HEIGHT; j++)
				{
					reserved[i][j] = world[i][j];
				}
			}
		}
	}

	this()
	{
		
	}

	// извлечение элемента
	auto opIndex(size_t i, size_t j)
	{
		return world[i][j];
	}

	// присвоение элемента
	void opIndexAssign(Element element, size_t i, size_t j)
	{
		world[i][j] = element;
	}

	// одно поколение клеточного автомата
	auto execute()
	{
		// скопировать мир
		backupWorld;

		// трансформация ячейки с проводником
		void transformConductorCell(int i, int j)
		{
			auto up = ((j + 1) >= WORLD_HEIGHT) ? WORLD_HEIGHT - 1 : j + 1;
			auto down = ((j - 1) < 0) ? 0 : j - 1;
			auto right = ((i + 1) >= WORLD_WIDTH) ?  WORLD_WIDTH - 1 : i + 1;
			auto left = ((i - 1) < 0) ? 0 : i - 1;

			auto counter = 0;

			if (reserved[i][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[i][down] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][j] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][j] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][down] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][down] == Element.Head)
			{
				counter++;
			}

			if ((counter == 1) || (counter == 2))
			{
				world[i][j] = Element.Head;
			}
			else
			{
				world[i][j] = Element.Conductor;
			}
		}

		for (int i = 0; i < WORLD_WIDTH; i++)
		{
			for (int j = 0; j < WORLD_HEIGHT; j++)
			{
				auto currentCell = reserved[i][j];
				
				final switch (currentCell) with (Element)
				{
					case Empty:
						world[i][j] = Empty;
						break;
					case Head:
						world[i][j] = Tail;
						break;
					case Tail:
						world[i][j] = Conductor;
						break;
					case Conductor:
						transformConductorCell(i, j);
						break;
				}
			}
		}
	}

	// очистка всего мира
	void clearWorld()
	{
		world = typeof(world).init;
	}

	// нарисовать мир с помощью QtE5
	void drawWorld(QPainter painter, int cellWidth, int cellHeight)
	{

		QColor BLACK = new QColor;
		QColor BLUE = new QColor;
		QColor RED = new QColor;
		QColor YELLOW = new QColor;
		QColor GRAY = new QColor;
		
		BLACK.setRgb(0, 0, 0, 230);
		BLUE.setRgb(0, 0, 255, 230);
		RED.setRgb(255, 0, 0, 230);
		YELLOW.setRgb(255, 255, 0, 230);
		GRAY.setRgb(133, 133, 133, 230);
		
		QPen pen = new QPen;
		pen.setColor(GRAY);

	    for (int i = 0; i < WORLD_WIDTH; i++)
		{
			for (int j = 0; j < WORLD_HEIGHT; j++)
			{
				auto currentCell = world[i][j];

			           // рисование прямоугольника
				QRect rect = new QRect;
	    		rect.setRect(i * cellWidth, j * cellHeight, cellWidth, cellHeight);

				final switch (currentCell) with (Element)
				{
					case Empty:
						painter.fillRect(rect, BLACK);			
						break;
					case Head:
						painter.fillRect(rect, BLUE);
						break;
					case Tail:
						painter.fillRect(rect, RED);
						break;
					case Conductor:
						painter.fillRect(rect, YELLOW);
						break;
				}

				painter.setPen(pen);
				painter.drawRect(i * cellWidth, j * cellHeight, cellWidth, cellHeight);
			}
		}
	}
}

Класс имеет пустой конструктор, так как все нужные параметры будут переданы в ходе формирования класса на этапе компиляции, а этого оказывается вполне достаточно для создания пространства в мире WireWorld. Для того, чтобы наш класс вел себя, точно также, как обычный двумерный массив (т.е. поддерживал операции извлечения по индексу и размещения по индексу), мы добавляем методы  opIndex и opIndexAssign. Кроме того, мы делаем реализацию «теневого копирования» backupWorld всего пространства клеточного автомата (мира) в скрытую внутреннюю переменную, что необходимо для осуществления анализа клеток в ходе работы правил перехода.

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

Для реализации правил перехода внутри цикла по всем элементам массива была применена одна из новых идиоматических конструкций с перечислениями final switch … with…,которая позволяет делать сопоставление по элементам перечисления без упоминания его имени (имя упоминается после with). Кроме того, использование final switch (эта конструкция работает только для перечислений) позволит с гарантией учесть все варианты перечисления, не пропустив при этом ни одного случая (рискните не упомянуть хотя бы один элемент перечисления в сопоставлении – увидите несколько новых матерных слов от компилятора).

Реализация правил довольно проста: выводим текущую клетку из резервной копии мира, а изменение по правилу перехода записываем в текущий мир. Но в случае, если у нас текущая клетка является проводником, то тут вступает в дело особое правило, представленное внутренней функцией transformConductorCell. Эта функция подсчитывает количество голов электрона в окрестности клетки, и если это количество равно 1 и 2, то проводник превращается в голову электрона, иначе все остается без изменений.

Метод clearWorld обнуляет весь мир (т.е. делает все клетки пустыми), а вот метод drawWorld задействует уже знакомую нам QtE5 для отображения мира в некотором графическом окне. Этот метод отобразит пространство WireWorld в некоторый виджет: пустая клетка будет отображена в черный прямоугольник, голова электрона – в синий, хвост электрона — в красный, а проводник станет желтым прямоугольником. Помимо этого, в метод передаются в качестве обязательных параметров длина и ширина одной клетки, а само пространство на основе их и будет «разлиновано» (сетка будет отображена).

Испытать только что созданный класс можно так:

WireWorld!(10, 10) wireWorld = new WireWorld!(10, 10);
writeln(wireWorld[1,1]);
wireWorld[1, 1] = Element.Head;
writeln(wireWorld[1,1]);

В данном примере, создается мир 10х10, затем считывается клетка с координатами (1, 1), та же самая клетка становится головой электрона.

Наблюдение за WireWorld в командной строке не отличается наглядностью, а ручной ввод данных в массив – это уже слишком…  Поэтому, используя одну очень хорошую библиотеку, а именно QtE5, создадим графический интерфейс для WireWorld и сделаем так, чтобы его клеточное пространство можно было редактировать с большим комфортом. Графический интерфейс будет содержать поле, которое будет отображать текущее состояние клеточного автомата, кнопку  “Load world ” (загрузка мира из файла) и две кнопки с говорящими названиями “Start” и  “Stop”. Кроме этого, для того, чтобы состояние автомата изменялось со временем, потребуется некоторое периодическое событие, которое можно будет запустить/остановить по нажатию на кнопку. Наличие такого события наталкивает на мысль о таймере, который не является визуальным компонентом, а значит, к интерфейсу добавляется и связанный с ним неграфический элемент. Также, требуется решить проблему несколько иного рода: в каком виджете отображать клеточное поле, ведь подходящего то в составе QtE5 нет. Следовательно, необходимо написать свой виджет, который можно назвать QWireWorld.

Вот код виджета:

// состояние мира (200х85)
WireWorld!(200,85) wireWorld;

extern(C)
{
    void onDrawStep(QWireWorld* wireWorldPointer, void* eventPointer, void* painterPointer) 
    { 
        (*wireWorldPointer).runDraw(eventPointer, painterPointer);
    }
}

class QWireWorld : QWidget
{
    private
    {
        QWidget parent;
    }

    this(QWidget parent)
    {
        wireWorld = new WireWorld!(200,85);
        super(parent);
        this.parent = parent;
        setStyleSheet(`background : white`);
        setPaintEvent(&onDrawStep, aThis);
        
    }

    void runDraw(void* eventPointer, void* painterPointer)
    {
      
        QPainter painter = new QPainter('+', painterPointer);

        wireWorld.drawWorld(painter, 10, 10);
       
        painter.end;
    }
}

Виджет прост: в начале задаем глобальную переменную, которая вместит в себя весь мир WireWorld, инициализируем ее внутри виджета, а затем определяем свою процедуру отрисовки виджета (что-то такое мы делали в статье про просмотрщик PPM-файлов) на основе предварительно подготовленного метода drawWorld объекта WireWorld.

Ну, и как мне показалось, оптимальным размером для клетки будет 10х10.

Теперь уже можно описывать сам интерфейс, даже с учетом таймера, который в QtE5 называется QTimer:

// псевдонимы под Qt'шные типы
alias WindowType = QtE.WindowType;

// основное окно
class MainForm : QWidget
{
    private
    {
        QVBoxLayout mainBox;
        QWireWorld box0;
        QPushButton button, button0, button1;
        QTimer timer;
        QAction action, action0, action1, action2;
    }

   
    this(QWidget parent, WindowType windowType) 
	{
		super(parent, windowType); 
		setWindowTitle("QWireWorld");

        mainBox = new QVBoxLayout(this);

        box0 = new QWireWorld(this);
        box0.saveThis(&box0);


       button = new QPushButton("Load world", this);
        button0 = new QPushButton("Start", this);
        button1 = new QPushButton("Stop", this);
        
        timer = new QTimer(this);
        timer.setInterval(100); 

        action = new QAction(this, &onLoadButton, aThis);
        connects(button, "clicked()", action, "Slot()");

        action0 = new QAction(this, &onTimerTick, aThis);
        connects(timer, "timeout()", action0, "Slot()");

        action1 = new QAction(null, &onStartButton, aThis);
        action2 = new QAction(null, &onStopButton, aThis);
        
        connects(button0, "clicked()", action1, "Slot()");
        connects(button1, "clicked()", action2, "Slot()");
        connects(button0, "clicked()", timer, "start()");
        connects(button1, "clicked()", timer, "stop()");

        mainBox
            .addWidget(box0)
            .addWidget(button)
            .addWidget(button0)
            .addWidget(button1);

        setLayout(mainBox);
    }

Собственно, ничего особенного: обычное размещение элементов, только не хватает обработчиков событий и переходников для них. Также, стоит немного обратить внимание на QTimer, для которого задается интервал спуска таймера в миллисекундах. Сигнал, который выдает таймер по истечении заданного интервала, называется timeout и его можно привязать к событию, которое сработает, когда таймер отсчитает свое время (делается это в функции connect). А сигналы запуска и остановки называются соответственно start и stop, и точно также, их можно привязать к нужному слоту и нужному элементу управления (в нашем случае, это кнопки запуска и остановки отрисовки).

Итак, сначала «переходники» к обработчикам:

extern(C)
{
    void onTimerTick(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runTimer;
    }

     void onStartButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStart;
    }

    void onStopButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStop;
    }

    void onLoadButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runLoad;
    }
}

Проще всего начать описывать обработчики с таймера:

void runTimer()
    {
       wireWorld.execute;
        box0.update;
    }

    void runStart()
    {
        button0.setEnabled(false);
        button1.setEnabled(true);
    }

    void runStop()
    {
        button0.setEnabled(true);
        button1.setEnabled(false);
    }

Каждый раз, когда таймер закончит отчет, будет вызван метод runTimer, который выполнит один шаг клеточного автомата и обновит поле его отображения. Методы  runStart и runStop сделают нужные кнопки недоступными, в случае нажатия на одну из них: нажатие на кнопку “Start” приведет к ее блокировке, тоже самое и для кнопки “Stop”.

Метод runLoad будет вызван при нажатии на кнопку “Load”, а его описание выглядит вот так:

void runLoad()
{
  import std.algorithm;
  import std.file;
  import std.range;
	import std.stdio;
	import std.string;

  QFileDialog fileDialog = new QFileDialog('+', null);
  string filename = fileDialog.getOpenFileNameSt("Open WireWorld File", "", "*.csv *.txt");

  if (!filename.empty)
  {
        	
    wireWorld.clearWorld;

    auto formatCSVString(string s)
		{
			import std.algorithm;
			import std.string;

			string replaceByE(string s)
			{
				return (s == "") ? "e" : s;
			}

			return s
						.split(";")
						.map!(a => replaceByE(a))
						.join;
		}

    auto content = (cast(string) std.file.read(filename))
        								.splitLines
        								.map!(a => formatCSVString(a))
        								.map!(a => toLower(a))
        								.array;

	  foreach (index, s; content)
	  {
	  	for (int i = 0; i < s.length; i++)
	  	{
	  		Element element;
	  		
	  		switch (s[i])
	  		{
	  			case 'e':
	  				element = Element.Empty;
	  				break;
	  			case 'h':
	  				element = Element.Head;
	  				break;
	  			case 't':
	  				element = Element.Tail;
	  				break;
	  			case 'c':
	  				element = Element.Conductor;
	  				break;
	  			default:
	  				break;
	  		}
	  		wireWorld[i, index] = element;
	  	}
	  }
  }
}

Что здесь происходит?

По нажатию на кнопку срабатывает создание и отображение объекта диалогового окна выбора файлов, после чего имя файла оказывается в соответствующей переменной. Если имя файла не пустое, то это значит, что интересующий нас файл существует, и мы подготавливаем мир WireWorld к загрузке (т.е. очищаем мир).

Предполагается, что файл, который мы выбрали – это CSV-файл, файл набор значений в котором значения разделяются точкой с запятой. Файл имеет очень простой формат – это таблица Microsoft Excel, импортированная в CSV (Опция при сохранении файла в Excel – CSV (разделители — запятые)), в которой пустые места выглядят пустыми клетками, головы электрона представлены клеткой с латинской буквой h (head), хвосты электрона – клеткой с латинской буквой t (tail), а проводники – клеткой с латинской c (conductor).

Именно, в таком формате подаются данные для заполнения мира (согласен, так рисовать схемы в WireWorld неудобно, но все-таки быстро привыкаешь и удобно отслеживать работу программы). Но тут есть свой подвох: CSV-файл – это обычный текстовой файл, в котором одна строка является одной строкой таблицы, а потому пустые ячейки таблицы представлены пустой строкой, как например, здесь:

Спрятано под спойлер
;t;c;c;c;c;c;;;;;;
h;;;;;;c;c;c;c;c;c;c
c;;;;;;c;;;;;;c
;c;c;c;c;c;c;;;;;;c
;;;;;;;;;;;;c
;;;;;;;;;;;;c
;;;;;;;;;;;;c
;c;c;c;c;c;c;c;c;c;c;;c
;c;;;;;;;;;c;;c
;c;;c;c;c;c;c;c;;c;;c
;c;;c;;;;;c;;c;;c
;c;;c;;c;c;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;;;c;;c;;c
;c;;c;;c;c;c;c;;c;;c
;c;;c;;;;;;;c;;c
;c;;c;c;c;c;c;c;c;c;;c
;c;;;;;;;;;;;c
;c;c;c;c;c;c;c;c;c;c;c;c

Кроме того, иногда, стоит явно подчеркивать пустые ячейки, а потому процедура загрузки опознает еще и символ e (empty) в CSV-файле, который обозначает пустую клетку. Из-за этого, внутри метода есть вспомогательная функция, которая форматирует CSV-строку файла, заменяя все пропуски на буквы e (строка разбивается по точкам с запятой, пропуски заменяются на букву “e”, а затем результирующий массив строк склеивается в единую строку).

CSV-файл загружается, превращаясь в строку, разбивается по разделителю «новая строка», дальше отдельные строки  форматируются согласно описанному выше, переводятся в нижний регистр и далее в цикле разбираются посимвольно и интерпретируются как элементы типа Element. Перестановка индексов элемента в инструкции wireWorld[i, index] = element; приводит к правильному порядку считывания данных из файла.

С помощью WireWorld можно симулировать различные электронные схемы, вот к примеру, найденный в одной из работ сумматор, который я лично переводил в CSV-файл (прилагается к статье):

WireWorld

Полный код, как обычно, под спойлером...

// элементы мира WireWorld
final enum Element : byte 
{
	Empty,        // пустое поле
	Head,         // голова электрона
	Tail,         // хвост электрона
	Conductor     // проводник
}

// мир WireWorld
class WireWorld(size_t WORLD_WIDTH, size_t WORLD_HEIGHT)
{
	private
	{
		// мир
		byte[WORLD_HEIGHT][WORLD_WIDTH] world;
		// копия мира
		byte[WORLD_HEIGHT][WORLD_WIDTH] reserved;

		// резервное копирование мира
		void backupWorld()
		{
			for (int i = 0; i < WORLD_WIDTH; i++)
			{
				for (int j = 0; j < WORLD_HEIGHT; j++)
				{
					reserved[i][j] = world[i][j];
				}
			}
		}
	}

	this()
	{
		
	}

	// извлечение элемента
	auto opIndex(size_t i, size_t j)
	{
		return world[i][j];
	}

	// присвоение элемента
	void opIndexAssign(Element element, size_t i, size_t j)
	{
		world[i][j] = element;
	}

	// одно поколение клеточного автомата
	auto execute()
	{
		// скопировать мир
		backupWorld;

		// трансформация ячейки с проводником
		void transformConductorCell(int i, int j)
		{
			auto up = ((j + 1) >= WORLD_HEIGHT) ? WORLD_HEIGHT - 1 : j + 1;
			auto down = ((j - 1) < 0) ? 0 : j - 1;
			auto right = ((i + 1) >= WORLD_WIDTH) ?  WORLD_WIDTH - 1 : i + 1;
			auto left = ((i - 1) < 0) ? 0 : i - 1;

			auto counter = 0;

			if (reserved[i][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[i][down] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][j] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][j] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[left][down] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][up] == Element.Head)
			{
				counter++;
			}

			if (reserved[right][down] == Element.Head)
			{
				counter++;
			}

			if ((counter == 1) || (counter == 2))
			{
				world[i][j] = Element.Head;
			}
			else
			{
				world[i][j] = Element.Conductor;
			}
		}

		for (int i = 0; i < WORLD_WIDTH; i++)
		{
			for (int j = 0; j < WORLD_HEIGHT; j++)
			{
				auto currentCell = reserved[i][j];
				
				final switch (currentCell) with (Element)
				{
					case Empty:
						world[i][j] = Empty;
						break;
					case Head:
						world[i][j] = Tail;
						break;
					case Tail:
						world[i][j] = Conductor;
						break;
					case Conductor:
						transformConductorCell(i, j);
						break;
				}
			}
		}
	}

	// очистка всего мира
	void clearWorld()
	{
		world = typeof(world).init;
	}

	// нарисовать мир с помощью QtE5
	void drawWorld(QPainter painter, int cellWidth, int cellHeight)
	{

	    QColor BLACK = new QColor;
	    QColor BLUE = new QColor;
	    QColor RED = new QColor;
	    QColor YELLOW = new QColor;
	    QColor GRAY = new QColor;

	  BLACK.setRgb(0, 0, 0, 230);
		BLUE.setRgb(0, 0, 255, 230);
		RED.setRgb(255, 0, 0, 230);
		YELLOW.setRgb(255, 255, 0, 230);
		GRAY.setRgb(133, 133, 133, 230);

		QPen pen = new QPen;
		pen.setColor(GRAY);

	    for (int i = 0; i < WORLD_WIDTH; i++)
		{
			for (int j = 0; j < WORLD_HEIGHT; j++)
			{
				auto currentCell = world[i][j];

				// рисование прямоугольника
				QRect rect = new QRect;
	    		rect.setRect(i * cellWidth, j * cellHeight, cellWidth, cellHeight);

				final switch (currentCell) with (Element)
				{
					case Empty:
						painter.fillRect(rect, BLACK);			
						break;
					case Head:
						painter.fillRect(rect, BLUE);
						break;
					case Tail:
						painter.fillRect(rect, RED);
						break;
					case Conductor:
						painter.fillRect(rect, YELLOW);
						break;
				}

				painter.setPen(pen);
				painter.drawRect(i * cellWidth, j * cellHeight, cellWidth, cellHeight);
			}
		}
	}
}

// состояние мира
WireWorld!(200,85) wireWorld;

extern(C)
{
    void onTimerTick(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runTimer;
    }

     void onStartButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStart;
    }

    void onStopButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStop;
    }

    void onLoadButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runLoad;
    }
}

extern(C)
{
    void onDrawStep(QWireWorld* wireWorldPointer, void* eventPointer, void* painterPointer) 
    { 
        (*wireWorldPointer).runDraw(eventPointer, painterPointer);
    }
}

class QWireWorld : QWidget
{
    private
    {
        QWidget parent;
    }

    this(QWidget parent)
    {
        wireWorld = new WireWorld!(200,85);
        super(parent);
        this.parent = parent;
        setStyleSheet(`background : white`);
        setPaintEvent(&onDrawStep, aThis);
        
    }

    void runDraw(void* eventPointer, void* painterPointer)
    {
      
        QPainter painter = new QPainter('+', painterPointer);

        wireWorld.drawWorld(painter, 10, 10);
       
        painter.end;
    }
}
 
// псевдонимы под Qt'шные типы
alias WindowType = QtE.WindowType;

// основное окно
class MainForm : QWidget
{
    private
    {
        QVBoxLayout mainBox;
        QWireWorld box0;
        QPushButton button, button0, button1;
        QTimer timer;
        QAction action, action0, action1, action2;
    }

   
    this(QWidget parent, WindowType windowType) 
	{
		super(parent, windowType); 
		setWindowTitle("QWireWorld");

        mainBox = new QVBoxLayout(this);

        box0 = new QWireWorld(this);
        box0.saveThis(&box0);


       	button = new QPushButton("Load world", this);
        button0 = new QPushButton("Start", this);
        button1 = new QPushButton("Stop", this);
        
        timer = new QTimer(this);
        timer.setInterval(100); 

        action = new QAction(this, &onLoadButton, aThis);
        connects(button, "clicked()", action, "Slot()");

        action0 = new QAction(this, &onTimerTick, aThis);
        connects(timer, "timeout()", action0, "Slot()");

        action1 = new QAction(null, &onStartButton, aThis);
        action2 = new QAction(null, &onStopButton, aThis);
        
        connects(button0, "clicked()", action1, "Slot()");
        connects(button1, "clicked()", action2, "Slot()");
        connects(button0, "clicked()", timer, "start()");
        connects(button1, "clicked()", timer, "stop()");

        mainBox
            .addWidget(box0)
            .addWidget(button)
            .addWidget(button0)
            .addWidget(button1);

        setLayout(mainBox);
    }

    void runTimer()
    {
    	wireWorld.execute;
        box0.update;
    }

    void runStart()
    {
        button0.setEnabled(false);
        button1.setEnabled(true);
    }

    void runStop()
    {
        button0.setEnabled(true);
        button1.setEnabled(false);
    }

    void runLoad()
    {
    	import std.algorithm;
    	import std.file;
    	import std.range;
		import std.stdio;
		import std.string;

    	QFileDialog fileDialog = new QFileDialog('+', null);
        string filename = fileDialog.getOpenFileNameSt("Open WireWorld File", "", "*.csv *.txt");

        if (!filename.empty)
        {
        	
        	wireWorld.clearWorld;

        	auto formatCSVString(string s)
			{
				import std.algorithm;
				import std.string;

				string replaceByE(string s)
				{
					return (s == "") ? "e" : s;
				}

				return s
						.split(";")
						.map!(a => replaceByE(a))
						.join;
			}

        	auto content = (cast(string) std.file.read(filename))
        								.splitLines
        								.map!(a => formatCSVString(a))
        								.map!(a => toLower(a))
        								.array;



	        foreach (index, s; content)
	        {
	        	for (int i = 0; i < s.length; i++)
	        	{
	        		Element element;
	        		
	        		switch (s[i])
	        		{
	        			case 'e':
	        				element = Element.Empty;
	        				break;
	        			case 'h':
	        				element = Element.Head;
	        				break;
	        			case 't':
	        				element = Element.Tail;
	        				break;
	        			case 'c':
	        				element = Element.Conductor;
	        				break;
	        			default:
	        				break;
	        		}
	        		wireWorld[i, index] = element;
	        	}
	        }
        }
    }
}

import core.runtime;
int QtDebugInfo(bool flag)
{
    return LoadQt(dll.QtE5Widgets, flag);
}

int main(string[] args) 
{
	QtDebugInfo(true);
	
	QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1);
	MainForm mainForm = new MainForm(null, normalWindow);
	
	mainForm.saveThis(&mainForm);	
	mainForm.showMaximized;
	
	return app.exec;
}

В общем, WireWorld – это забавная штука, а также очень хороший пример работы с таймером в QtE5!

Обещанная схема: Сумматор для WireWorld

Добавить комментарий