1
0
forked from IPKM/nmreval

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} %')