forked from IPKM/nmreval
381 lines
12 KiB
Python
381 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import numbers
|
|
import numpy as np
|
|
import sys
|
|
from itertools import accumulate
|
|
|
|
from ..Qt import QtWidgets, QtGui, QtCore
|
|
from .._py.gol import Ui_Form
|
|
|
|
|
|
def circle(radius):
|
|
pxl = []
|
|
for x in range(int(np.ceil(radius/np.sqrt(2)))):
|
|
y = round(np.sqrt(radius**2-x**2))
|
|
pxl.extend([[x, y], [y, x], [x, -y], [y, -x],
|
|
[-x, -y], [-y, -x], [-x, y], [-y, x]])
|
|
|
|
return np.array(pxl)
|
|
|
|
|
|
def square(a):
|
|
pxl = []
|
|
pxl.extend(list(zip(range(-a, a+1), [a]*(2*a+1))))
|
|
pxl.extend(list(zip(range(-a, a+1), [-a]*(2*a+1))))
|
|
pxl.extend(list(zip([a]*(2*a+1), range(-a, a+1))))
|
|
pxl.extend(list(zip([-a]*(2*a+1), range(-a, a+1))))
|
|
|
|
return np.array(pxl)
|
|
|
|
|
|
def diamond(a):
|
|
pxl = []
|
|
for x in range(int(a+1)):
|
|
y = a-x
|
|
pxl.extend([[x, y], [-x, y], [x, -y], [-x, -y]])
|
|
|
|
return np.array(pxl)
|
|
|
|
|
|
def plus(a):
|
|
pxl = np.zeros((4*int(a)+2, 2), dtype=int)
|
|
pxl[:2*int(a)+1, 0] = np.arange(-a, a+1)
|
|
pxl[2*int(a)+1:, 1] = np.arange(-a, a+1)
|
|
|
|
return pxl
|
|
|
|
# birth, survival
|
|
predefined_rules = {
|
|
'Conway': ('23', '3'),
|
|
'Life34': ('34', '34'),
|
|
'Coagulation': ('235678', '378'),
|
|
'Corrosion': ('12345', '45'),
|
|
'Long life': ('5', '345'),
|
|
'Maze': ('12345', '3'),
|
|
'Coral': ('45678', '3'),
|
|
'Pseudo life': ('238', '357'),
|
|
'Flakes': ('012345678', '3'),
|
|
'Gnarl': ('1', '1'),
|
|
'Fabric': ('12', '1'),
|
|
'Assimilation': ('4567', '345'),
|
|
'Diamoeba': ('5678', '35678'),
|
|
'High life': ('23', '36'),
|
|
'More Maze': ('1235', '3'),
|
|
'Replicator': ('1357', '1357'),
|
|
'Seed': ('', '2'),
|
|
'Serviette': ('', '234'),
|
|
'More coagulation': ('235678', '3678'),
|
|
'Domino': ('125', '36'),
|
|
'Anneal': ('35678', '4678'),
|
|
}
|
|
|
|
|
|
class GameOfLife:
|
|
colors = [
|
|
[31, 119, 180],
|
|
[255, 127, 14],
|
|
[44, 160, 44],
|
|
[214, 39, 40],
|
|
[148, 103, 189],
|
|
[140, 86, 75]
|
|
]
|
|
|
|
def __init__(self,
|
|
size: tuple = (400, 400),
|
|
pattern: np.ndarray = None,
|
|
fill: float | list[float] = 0.05,
|
|
num_pop: int = 1,
|
|
rules: str | tuple = ('23', '3')
|
|
):
|
|
self.populations = num_pop if pattern is None else 1
|
|
self._size = size
|
|
|
|
self._world = np.zeros(shape=(*self._size, self.populations), dtype=np.uint8)
|
|
self._neighbors = np.zeros(shape=self._world.shape, dtype=np.uint8)
|
|
self._drawing = 255 * np.zeros(shape=(*self._size, 3), dtype=np.uint8)
|
|
|
|
self.fill = np.zeros(self.populations)
|
|
self._populate(fill, pattern)
|
|
|
|
if isinstance(rules, str):
|
|
try:
|
|
b_rule, s_rule = predefined_rules[rules]
|
|
except KeyError:
|
|
raise ValueError('Rule is not predefined')
|
|
else:
|
|
b_rule, s_rule = rules
|
|
|
|
self.survival_condition = np.array([int(c) for c in s_rule])
|
|
self.birth_condition = np.array([int(c) for c in b_rule])
|
|
|
|
# indexes for neighbors
|
|
self._neighbor_idx = [
|
|
[[slice(None), slice(1, None)], [slice(None), slice(0, -1)]], # N (:, 1:), S (:, _-1)
|
|
[[slice(1, None), slice(1, None)], [slice(0, -1), slice(0, -1)]], # NE (1:, 1:), SW (:-1, :-1)
|
|
[[slice(1, None), slice(None)], [slice(0, -1), slice(None)]], # E (1:, :), W (:-1, :)
|
|
[[slice(1, None), slice(0, -1)], [slice(0, -1), slice(1, None)]] # SE (1:, :-1), NW (:-1:, 1:)
|
|
]
|
|
|
|
def _populate(self, fill, pattern):
|
|
if pattern is None:
|
|
if isinstance(fill, numbers.Number):
|
|
fill = [fill]*self.populations
|
|
|
|
prob = (np.random.default_rng().random(size=self._size))
|
|
lower_lim = 0
|
|
for i, upper_lim in enumerate(accumulate(fill)):
|
|
self._world[:, :, i] = (lower_lim <= prob) & (prob < upper_lim)
|
|
lower_lim = upper_lim
|
|
|
|
else:
|
|
pattern = np.asarray(pattern)
|
|
|
|
x_step = self._size[0]//(self.populations+1)
|
|
y_step = self._size[1]//(self.populations+1)
|
|
|
|
for i in range(self.populations):
|
|
self._world[-pattern[:, 0]+(i+1)*x_step, pattern[:, 1]+(i+1)*y_step, i] = 1
|
|
|
|
for i in range(self.populations):
|
|
self.fill[i] = self._world[:, :, i].sum() / (self._size[0]*self._size[1])
|
|
|
|
def tick(self):
|
|
n = self._neighbors
|
|
w = self._world
|
|
|
|
n[...] = 0
|
|
for idx_1, idx_2 in self._neighbor_idx:
|
|
n[tuple(idx_1)] += w[tuple(idx_2)]
|
|
n[tuple(idx_2)] += w[tuple(idx_1)]
|
|
|
|
birth = ((np.in1d(n, self.birth_condition).reshape(n.shape)) & (w.sum(axis=-1) == 0)[:, :, None])
|
|
survive = ((np.in1d(n, self.survival_condition).reshape(n.shape)) & (w == 1))
|
|
|
|
w[...] = 0
|
|
w[birth | survive] = 1
|
|
|
|
def draw(self, shade: int):
|
|
if shade == 0:
|
|
self._drawing[...] = 0
|
|
elif shade == 1:
|
|
self._drawing -= (self._drawing/4).astype(np.uint8)
|
|
self._drawing = self._drawing.clip(0, 127)
|
|
|
|
for i in range(self.populations):
|
|
self._drawing[(self._world[:, :, i] == 1)] = self.colors[i]
|
|
self.fill[i] = self._world[:, :, i].sum() / (self._size[0]*self._size[1])
|
|
|
|
return self._drawing
|
|
|
|
|
|
class QGameOfLife(QtWidgets.QDialog, Ui_Form):
|
|
SPEEDS = [0.5, 1, 2, 5, 7.5, 10, 12.5, 15, 20, 25, 30, 40, 50]
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
self.setupUi(self)
|
|
|
|
self._size = (100, 100)
|
|
self.game = None
|
|
self._step = 0
|
|
self._shading = 1
|
|
self._speed = 5
|
|
|
|
self.timer = QtCore.QTimer()
|
|
self.timer.setInterval(100)
|
|
self.timer.timeout.connect(self.tick)
|
|
|
|
self._init_ui()
|
|
|
|
def _init_ui(self):
|
|
self.item = None
|
|
self.scene = QtWidgets.QGraphicsScene()
|
|
self.item = self.scene.addPixmap(QtGui.QPixmap())
|
|
self.view.setScene(self.scene)
|
|
|
|
self.rule_cb.addItems(list(predefined_rules.keys()))
|
|
|
|
self.random_widgets = []
|
|
for _ in range(6):
|
|
w = QSliderText(15, parent=self)
|
|
w.slider.valueChanged.connect(self.set_max_population)
|
|
self.verticalLayout.addWidget(w)
|
|
self.random_widgets.append(w)
|
|
|
|
self.birth_line.setValidator(QtGui.QIntValidator())
|
|
self.survival_line.setValidator(QtGui.QIntValidator())
|
|
|
|
for w in self.random_widgets[1:] + [self.object_widget]:
|
|
w.hide()
|
|
|
|
self.setGeometry(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter,
|
|
self.size(), QtWidgets.qApp.desktop().availableGeometry()))
|
|
|
|
self.view.resizeEvent = self.resizeEvent
|
|
|
|
@QtCore.pyqtSlot(int)
|
|
def on_object_combobox_currentIndexChanged(self, idx: int):
|
|
for w in self.random_widgets + [self.object_widget, self.rand_button_wdgt]:
|
|
w.hide()
|
|
|
|
if idx == 0:
|
|
self.random_widgets[0].show()
|
|
self.rand_button_wdgt.show()
|
|
else:
|
|
self.object_widget.show()
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_add_random_button_clicked(self):
|
|
if self.object_combobox.currentIndex() != 0:
|
|
return
|
|
|
|
for w in self.random_widgets[1:]:
|
|
if not w.isVisible():
|
|
w.show()
|
|
break
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_remove_random_button_clicked(self):
|
|
if self.object_combobox.currentIndex() != 0:
|
|
return
|
|
|
|
for w in reversed(self.random_widgets[1:]):
|
|
if w.isVisible():
|
|
w.hide()
|
|
break
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
def on_rule_cb_currentIndexChanged(self, entry: str):
|
|
rule = predefined_rules[entry]
|
|
self.birth_line.setText(rule[1])
|
|
self.survival_line.setText(rule[0])
|
|
|
|
@QtCore.pyqtSlot(int)
|
|
def set_max_population(self, _: int):
|
|
over_population = -100
|
|
num_tribes = -1
|
|
for w in self.random_widgets:
|
|
if w.isVisible():
|
|
over_population += w.slider.value()
|
|
num_tribes += 1
|
|
|
|
if over_population > 0:
|
|
for w in self.random_widgets:
|
|
if w == self.sender() or w.isHidden():
|
|
continue
|
|
w.setValue(max(0, int(w.slider.value()-over_population/num_tribes)))
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_start_button_clicked(self):
|
|
self.pause_button.setChecked(False)
|
|
self._step = 0
|
|
self.current_step.setText(f'{self._step} steps')
|
|
self._size = (int(self.height_box.value()), int(self.width_box.value()))
|
|
|
|
pattern = None
|
|
num_pop = 0
|
|
fill = []
|
|
pattern_size = self.object_size.value()
|
|
if 2*pattern_size >= max(self._size):
|
|
pattern_size = int(np.floor(max(self._size[0]-1, self._size[1]-1) / 2))
|
|
|
|
idx = self.object_combobox.currentIndex()
|
|
if idx == 0:
|
|
for w in self.random_widgets:
|
|
if w.isVisible():
|
|
num_pop += 1
|
|
fill.append(w.slider.value()/100)
|
|
else:
|
|
pattern = [None, circle, square, diamond, plus][idx](pattern_size)
|
|
|
|
self.game = GameOfLife(self._size, pattern=pattern, fill=fill, num_pop=num_pop,
|
|
rules=(self.birth_line.text(), self.survival_line.text()))
|
|
self.draw()
|
|
self.view.fitInView(self.item, QtCore.Qt.KeepAspectRatio)
|
|
|
|
self.timer.start()
|
|
|
|
def tick(self):
|
|
self.game.tick()
|
|
self.draw()
|
|
|
|
self._step += 1
|
|
self.current_step.setText(f'{self._step} steps')
|
|
self.cover_label.setText('\n'.join([f'Color {i+1}: {f*100:.2f} %' for i, f in enumerate(self.game.fill)]))
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_faster_button_clicked(self):
|
|
self._speed = min(self._speed+1, len(QGameOfLife.SPEEDS)-1)
|
|
new_speed = QGameOfLife.SPEEDS[self._speed]
|
|
self.timer.setInterval(int(1000/new_speed))
|
|
self.velocity_label.setText(f'{new_speed:.1f} steps/s')
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_slower_button_clicked(self):
|
|
self._speed = max(self._speed-1, 0)
|
|
new_speed = QGameOfLife.SPEEDS[self._speed]
|
|
self.timer.setInterval(int(1000/new_speed))
|
|
self.velocity_label.setText(f'{new_speed:.1f} steps/s')
|
|
|
|
@QtCore.pyqtSlot()
|
|
def on_pause_button_clicked(self):
|
|
if self.pause_button.isChecked():
|
|
self.timer.stop()
|
|
else:
|
|
self.timer.start()
|
|
|
|
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
|
|
def on_buttonGroup_buttonClicked(self, button):
|
|
self._shading = [self.radioButton, self.vanish_shadow, self.full_shadow].index(button)
|
|
|
|
def draw(self):
|
|
bitmap = self.game.draw(shade=self._shading)
|
|
h, w, c = bitmap.shape
|
|
image = QtGui.QImage(bitmap.tobytes(), w, h, w*c, QtGui.QImage.Format_RGB888)
|
|
self.scene.removeItem(self.item)
|
|
pixmap = QtGui.QPixmap.fromImage(image)
|
|
self.item = self.scene.addPixmap(pixmap)
|
|
|
|
@QtCore.pyqtSlot(int)
|
|
def on_hide_button_stateChanged(self, state: int):
|
|
self.option_frame.setVisible(not state)
|
|
self.view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
|
|
|
|
def resizeEvent(self, evt):
|
|
super().resizeEvent(evt)
|
|
self.view.fitInView(self.item, QtCore.Qt.KeepAspectRatio)
|
|
|
|
def showEvent(self, evt):
|
|
super().showEvent(evt)
|
|
self.view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
|
|
|
|
|
|
class QSliderText(QtWidgets.QWidget):
|
|
def __init__(self, value: int, parent=None):
|
|
super().__init__(parent=parent)
|
|
layout = QtWidgets.QHBoxLayout()
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(3)
|
|
|
|
self.slider = QtWidgets.QSlider(self)
|
|
self.slider.setOrientation(QtCore.Qt.Horizontal)
|
|
self.slider.setMaximum(100)
|
|
self.slider.setTickPosition(self.slider.TicksBothSides)
|
|
self.slider.setTickInterval(5)
|
|
|
|
self.value = QtWidgets.QLabel(self)
|
|
|
|
self.slider.valueChanged.connect(lambda x: self.value.setText(f'{x} %'))
|
|
|
|
layout.addWidget(self.slider)
|
|
layout.addWidget(self.value)
|
|
|
|
self.setLayout(layout)
|
|
|
|
self.setValue(value)
|
|
|
|
def setValue(self, value: int):
|
|
self.slider.setValue(value)
|
|
self.value.setText(f'{value} %')
|