Conway's Game of Life
A implementation of Conway's Game of Life cellular automaton (and variants).
This example demonstrates providing live 2D data efficiently from Python to QML.
Files to load into this simulator can be found online, e.g. going to https://conwaylife.com/wiki/Gosper_glider_gun and downloading the "RLE" file under "Pattern files" in the right sidebar.
// SPDX-FileCopyrightText: Copyright (c) 2024 Refeyn Ltd and other QuickGraphLib contributors // SPDX-License-Identifier: MIT import QtQuick import QtQuick.Layouts as QQL import QtQuick.Dialogs as QQD import QtQuick.Controls as QQC import QuickGraphLib as QuickGraphLib import examples.Conway as ConwayProvider QQL.ColumnLayout { QQD.FileDialog { id: loadDialog fileMode: QQD.FileDialog.OpenFile nameFilters: ["RLE files (*.rle)", "All files (*)"] title: "Load RLE pattern" onAccepted: conwayProvider.loadFile(selectedFile) } Timer { interval: 1000 / 60 repeat: true running: true onTriggered: gc()// Prevent JS hoarding too many frames, since it doesn't know how big they are } Timer { interval: intervalSpin.value repeat: true running: runButton.checked onTriggered: conwayProvider.tick() } ConwayProvider.ConwayProvider { id: conwayProvider } QQL.RowLayout { QQC.Button { text: "Load pattern" onClicked: loadDialog.open() } QQC.Button { text: "Reset" onClicked: conwayProvider.reset() } QQC.Button { text: "Step" onClicked: conwayProvider.tick() } QQC.Button { id: runButton checkable: true text: checked ? "Pause" : "Run" } QQC.Button { text: "Rotate 90°" onClicked: conwayProvider.rot90() } Item { QQL.Layout.fillWidth: true } QQC.Label { text: "Display mode" } QQC.ComboBox { id: displayModeCombo model: [ { "value": "historyCells", "text": "Cells with history" }, { "value": "cells", "text": "Cells" }, { "value": "neighbourCounts", "text": "Neighbour counts" } ] textRole: "text" valueRole: "value" } } Item { QQL.Layout.fillHeight: true QQL.Layout.fillWidth: true clip: true QuickGraphLib.ZoomPanHandler { id: pinchArea anchors.fill: parent } QuickGraphLib.ImageView { anchors.fill: parent colormap: QuickGraphLib.ColorMaps.Magma fillMode: Qt.KeepAspectRatio source: conwayProvider[displayModeCombo.currentValue || "cells"] sourceSize: conwayProvider.size transform: Matrix4x4 { matrix: pinchArea.viewTransform } } } QQL.RowLayout { QQC.Label { text: "ms per tick" } QQC.SpinBox { id: intervalSpin editable: true from: 1 to: 2000 value: 100 } QQC.Label { text: "Size" } QQC.SpinBox { id: widthSpin editable: true from: 1 to: 2000 value: conwayProvider.size.width onValueModified: conwayProvider.size = Qt.size(widthSpin.value, heightSpin.value) } QQC.Label { text: "x" } QQC.SpinBox { id: heightSpin editable: true from: 1 to: 2000 value: conwayProvider.size.height onValueModified: conwayProvider.size = Qt.size(widthSpin.value, heightSpin.value) } QQC.Label { text: "Rule B" } QQC.TextField { implicitWidth: 75 maximumLength: 9 text: conwayProvider.ruleB validator: RegularExpressionValidator { regularExpression: /[0-8]*/ } onEditingFinished: conwayProvider.ruleB = text } QQC.Label { text: "/S" } QQC.TextField { implicitWidth: 75 maximumLength: 9 text: conwayProvider.ruleS validator: RegularExpressionValidator { regularExpression: /[0-8]*/ } onEditingFinished: conwayProvider.ruleS = text } } }
# SPDX-FileCopyrightText: Copyright (c) 2024 Refeyn Ltd and other QuickGraphLib contributors # SPDX-License-Identifier: MIT import enum import itertools import pathlib import re from typing import List, Tuple import numpy as np from PySide6 import QtCore, QtQml from QuickGraphLib import QGLDoubleList QML_IMPORT_NAME = "examples.Conway" QML_IMPORT_MAJOR_VERSION = 1 QML_IMPORT_MINOR_VERSION = 0 @QtQml.QmlElement class ConwayProvider(QtCore.QObject): class CellTransition(enum.IntFlag): SURVIVE = 1 BORN = 2 DIE = 0 BORN_AND_SURVIVE = SURVIVE | BORN RULE_LENGTH = 9 FILE_LOAD_PADDING = 2 def __init__(self): super().__init__() self._buffer = np.zeros((100, 100), dtype=bool) self._history_buffer = np.zeros_like(self._buffer, dtype=int) self._neighbour_counts = np.zeros_like(self._buffer, dtype=int) # Standard game is B3/S23 self._rule = self._rule_from_strs("3", "23") sizeChanged = QtCore.Signal(name="sizeChanged") @QtCore.Property(QtCore.QSize, notify=sizeChanged) def size(self) -> QtCore.QSize: return QtCore.QSize(*self._buffer.shape[::-1]) @size.setter # type: ignore[no-redef] def size(self, size: QtCore.QSize) -> None: left_y = max(0, round((size.height() - self._buffer.shape[0]) / 2)) right_y = max(0, round((self._buffer.shape[0] - size.height()) / 2)) min_height = min(self._buffer.shape[0], size.height()) left_x = max(0, round((size.width() - self._buffer.shape[1]) / 2)) right_x = max(0, round((self._buffer.shape[1] - size.width()) / 2)) min_width = min(self._buffer.shape[1], size.width()) new_buffer = np.zeros((size.height(), size.width()), dtype=bool) new_buffer[left_y : left_y + min_height, left_x : left_x + min_width] = ( self._buffer[right_y : right_y + min_height, right_x : right_x + min_width] ) self._buffer = new_buffer new_history_buffer = np.zeros((size.height(), size.width()), dtype=int) new_history_buffer[ left_y : left_y + min_height, left_x : left_x + min_width ] = self._history_buffer[ right_y : right_y + min_height, right_x : right_x + min_width ] self._history_buffer = new_history_buffer new_neighbour_counts = np.zeros((size.height(), size.width()), dtype=int) new_neighbour_counts[ left_y : left_y + min_height, left_x : left_x + min_width ] = self._neighbour_counts[ right_y : right_y + min_height, right_x : right_x + min_width ] self._neighbour_counts = new_neighbour_counts self.sizeChanged.emit() self.dataChanged.emit() ruleChanged = QtCore.Signal(name="ruleChanged") @QtCore.Property(str, notify=ruleChanged) def ruleB(self) -> str: return self._strs_from_rule()[0] @ruleB.setter # type: ignore[no-redef] def ruleB(self, rule_b: str) -> None: self._rule = self._rule_from_strs(rule_b, self._strs_from_rule()[1]) self.ruleChanged.emit() @QtCore.Property(str, notify=ruleChanged) def ruleS(self) -> str: return self._strs_from_rule()[1] @ruleS.setter # type: ignore[no-redef] def ruleS(self, rule_s: str) -> None: self._rule = self._rule_from_strs(self._strs_from_rule()[0], rule_s) self.ruleChanged.emit() dataChanged = QtCore.Signal(name="dataChanged") @QtCore.Property(QGLDoubleList, notify=dataChanged) # type: ignore[operator, arg-type] def cells(self) -> QGLDoubleList: return QGLDoubleList.fromNDArray(self._buffer.ravel().astype(int)) @QtCore.Property(QGLDoubleList, notify=dataChanged) # type: ignore[operator, arg-type] def historyCells(self) -> QGLDoubleList: return QGLDoubleList.fromNDArray(self._history_buffer.ravel()) @QtCore.Property(QGLDoubleList, notify=dataChanged) # type: ignore[operator, arg-type] def neighbourCounts(self) -> QGLDoubleList: return QGLDoubleList.fromNDArray(self._neighbour_counts.ravel()) @QtCore.Slot() def tick(self) -> None: self._neighbour_counts = np.zeros(self._buffer.shape, dtype=int) for y_shift, x_shift in itertools.product([-1, 0, 1], repeat=2): if y_shift or x_shift: self._neighbour_counts += np.roll( self._buffer, (y_shift, x_shift), (0, 1) ) for i, rule in enumerate(self._rule): if rule == self.CellTransition.BORN_AND_SURVIVE: self._buffer[self._neighbour_counts == i] = True elif rule == self.CellTransition.BORN: self._buffer[self._neighbour_counts == i] = ~self._buffer[ self._neighbour_counts == i ] elif rule == self.CellTransition.SURVIVE: pass elif rule == self.CellTransition.DIE: self._buffer[self._neighbour_counts == i] = False self._history_buffer *= 95 self._history_buffer //= 100 self._history_buffer[self._buffer] = 1000 self.dataChanged.emit() @QtCore.Slot() def reset(self) -> None: self._buffer = np.zeros((100, 100), dtype=bool) self._history_buffer = np.zeros_like(self._buffer, dtype=int) self._neighbour_counts = np.zeros_like(self._buffer, dtype=int) self.dataChanged.emit() self.sizeChanged.emit() @QtCore.Slot() def rot90(self) -> None: self._buffer = np.rot90(self._buffer) self._history_buffer = np.rot90(self._history_buffer) self._neighbour_counts = np.rot90(self._neighbour_counts) self.dataChanged.emit() self.sizeChanged.emit() def _rule_from_strs(self, rule_b: str, rule_s: str) -> List[CellTransition]: rule = [self.CellTransition.DIE] * self.RULE_LENGTH for num in rule_s: rule[int(num)] |= self.CellTransition.SURVIVE for num in rule_b: rule[int(num)] |= self.CellTransition.BORN return rule def _strs_from_rule(self) -> Tuple[str, str]: rule_s = "".join( [ str(i) for i in range(self.RULE_LENGTH) if self._rule[i] & self.CellTransition.SURVIVE ] ) rule_b = "".join( [ str(i) for i in range(self.RULE_LENGTH) if self._rule[i] & self.CellTransition.BORN ] ) return rule_b, rule_s @QtCore.Slot(QtCore.QUrl) def loadFile(self, url: QtCore.QUrl) -> None: path = pathlib.Path(url.toLocalFile()) if path.suffix != ".rle": print("Unrecognized file type") return data = path.read_text(encoding="ascii") seen_header = False initial_x = current_x = current_y = 0 buffer = np.zeros((0, 0), dtype=bool) rule_b = rule_s = None for line in data.split("\n"): if line.startswith("#") or not line.strip(): pass elif not seen_header: line_match = re.match( r"\s*x\s*=\s*(\d+),\s*y\s*=\s*(\d+)(?:,\s*rule\s*=\s*B(\d*)/S(\d*))?", line, ) if not line_match: print("Could not interpret file header") return x, y, rule_b, rule_s = line_match.groups() x, y = int(x), int(y) buffer = np.zeros( ( max(self._buffer.shape[0], y + self.FILE_LOAD_PADDING * 2), max(self._buffer.shape[1], x + self.FILE_LOAD_PADDING * 2), ), dtype=bool, ) initial_x = current_x = (buffer.shape[1] - x) // 2 current_y = (buffer.shape[0] - y) // 2 seen_header = True else: line = line.strip() while line and line != "!": tag = re.match(r"(\d*)([a-zA-Z$])", line) if not tag: print("Could not interpret RLE sequence item") return times = int(tag.group(1) or "1") op = tag.group(2) if tag.group(2) in "ob$" else "o" for _ in range(times): if op == "$": current_y += 1 current_x = initial_x else: if op == "o": buffer[(current_y, current_x)] = True current_x += 1 line = line[tag.end() :].strip() self._buffer = buffer self._history_buffer = self._buffer.astype(int) self._neighbour_counts = np.zeros_like(self._buffer, dtype=int) self._rule = self._rule_from_strs( "3" if rule_b is None else rule_b, "23" if rule_s is None else rule_s ) self.sizeChanged.emit() self.dataChanged.emit() self.ruleChanged.emit()