Skip to content

Instantly share code, notes, and snippets.

@JeanRibes
Last active June 26, 2021 22:24
Show Gist options
  • Save JeanRibes/fef879b6f71023e15e00b7ba5a5fa607 to your computer and use it in GitHub Desktop.
Save JeanRibes/fef879b6f71023e15e00b7ba5a5fa607 to your computer and use it in GitHub Desktop.
Conway's Game of Life as a QGIS layer
"""
Conway's Game of Life implemented in QGIS vectorlayer
How to use: run in terminal:
qgis --code qgis_conway.py
"""
from random import randint, choice, random
from threading import Lock
import qgis
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QAction
from qgis.core import QgsPolygon, QgsProject, QgsFeature, QgsVectorLayer
from qgis.utils import iface
GLIDER = [
(0, 0),
(0, 1),
(0, 2),
(1, 2),
(2, 1),
]
COMPANION_CUBE = [
(0, 2), (0, 3), (0, 4), (0, 8), (0, 9), (0, 10),
(2, 0), (2, 5), (2, 7), (2, 12),
(3, 0), (3, 5), (3, 7), (3, 12),
(4, 0), (4, 5), (4, 7), (4, 12),
(5, 2), (5, 3), (5, 4), (5, 8), (5, 9), (5, 10),
(7, 2), (7, 3), (7, 4), (7, 8), (7, 9), (7, 10),
(6 + 2, 0), (6 + 2, 5), (6 + 2, 7), (6 + 2, 12),
(6 + 3, 0), (6 + 3, 5), (6 + 3, 7), (6 + 3, 12),
(6 + 4, 0), (6 + 4, 5), (6 + 4, 7), (6 + 4, 12),
(13, 2), (13, 3), (13, 4), (13, 8), (13, 9), (13, 10),
]
F = [
(0, 1), (0, 2),
(1, 0), (1, 1),
(2, 1)
]
ACORN = [
(0, 1),
(1, 3),
(2, 0), (2, 1), (2, 4), (2, 5), (2, 6)
]
SHAPES = [ACORN, GLIDER, F]
class Conway:
def __init__(self, size):
self.size = size
self.cells = [[False for _ in range(size)] for _ in range(size)]
def add_shape(self, xoff, yoff, shape):
for x, y in shape:
self((y + yoff) % self.size, (x + xoff) % self.size)
def shuffle(self, num):
for _ in range(num):
row = randint(0, self.size - 1)
col = randint(0, self.size - 1)
self.add_shape(row, col, choice(SHAPES))
def step(self):
next_cells = [[False for _ in range(self.size)] for _ in range(self.size)]
changes = []
for ir, row in enumerate(self.cells):
for ic, col in enumerate(row):
alive = col
new_state = self.action(ir, ic, alive)
next_cells[ir][ic] = new_state
if new_state != alive:
changes.append((ir, ic, new_state))
self.cells = next_cells
if random() > 0.98:
self.shuffle(4)
return changes
def action(self, row, col, was_alive):
around = []
for drow, dcol in [
(-1, 1),
(0, 1),
(1, 1),
(-1, 0), #
# (0,0),
(1, 0), #
(-1, -1),
(0, -1),
(1, -1),
]:
try:
around.append(self.cells[row + drow][col + dcol])
except IndexError:
pass
alive_around = around.count(True)
if alive_around == 3:
return True
elif alive_around < 2 or alive_around > 3:
return False
else:
return was_alive
def print(self):
print('-' * self.size * 3)
for row in self.cells:
for cell in row:
print('|x' if cell else '| ', end='')
print('|')
print('-' * self.size * 3)
def to_polycoords(self, x0, y0, w, h):
polygons = []
for ir, row in enumerate(self.cells):
for ic, cell in enumerate(row):
if cell:
polygons.append(
self.cell_to_polycoord(ir, ic, x0, y0, w, h))
return polygons
def cell_to_polycoord(self, ir, ic, x0, y0, w, h):
return (
(x0 + ir * w, y0 + ic * h),
(x0 + (ir + 1) * w, y0 + ic * h),
(x0 + (ir + 1) * w, y0 + (ic + 1) * h),
(x0 + ir * w, y0 + (ic + 1) * h),
)
def cell_wkt(self, poly, *args):
coords = ', '.join(f"{x} {y}" for x, y in poly)
return f"POLYGON (({coords}))"
def to_wkt(self, *args):
for poly in self.to_polycoords(*args):
yield self.cell_wkt(poly, *args)
def __call__(self, row: int, col: int):
self.cells[row][col] = True
def checkerboard(self):
for i in range(self.size):
for j in range(self.size):
if (j + i) % 2:
q(i, j)
class QgsConway(Conway):
layer: QgsVectorLayer
def __init__(self, size, x0=-250000, y0=5200000, cellwidth=20000, cellheight=20000, layer_name='conway'):
self.x0 = x0
self.y0 = y0
self.cellwidth = cellwidth
self.cellheight = cellheight
super().__init__(size)
self.layer = QgsProject.instance().mapLayersByName(layer_name)[0]
self.lock = Lock()
self.feats = [[None for _ in range(size)] for _ in range(size)]
def cell_to_feat(self, ir, ic):
poly = QgsPolygon()
poly.fromWkt(
wkt=self.cell_wkt(self.cell_to_polycoord(ir, ic, self.x0, self.y0, self.cellwidth, self.cellheight)))
feat = QgsFeature()
feat.setGeometry(poly)
feat.setAttributes([ir, ic])
# feat.setAttribute('row',ir)
# feat.setAttribute('col',ic)
self.feats[ir][ic] = feat.id()
return feat
def to_polygons(self):
l = []
for ir, row in enumerate(self.cells):
for ic, cell in enumerate(row):
if cell:
l.append(self.cell_to_feat(ir, ic))
return l
def show(self):
self.layer.dataProvider().truncate()
self.layer.startEditing()
self.layer.addFeatures(self.to_polygons())
self.layer.commitChanges()
def one(self):
with self.lock:
changes = self.step()
self.layer.startEditing()
deads = []
for ir, ic, alive in changes:
if alive:
self.layer.addFeature(self.cell_to_feat(ir, ic))
else:
deads.append([ir, ic])
# self.layer.commitChanges()
# self.layer.startEditing()
for feat in self.layer.getFeatures():
attr = feat.attributes()
if attr in deads:
self.layer.deleteFeature(feat.id())
self.feats[ir][ic] = None
self.layer.commitChanges()
def more(self):
with self.lock:
self.shuffle(1)
self.show()
if __name__ == '__main__':
iface.addVectorLayer('/usr/share/qgis/resources/data/world_map.gpkg|layername=countries', "world", 'ogr')
layer = QgsVectorLayer("Polygon?crs=epsg:3857&field=row:integer&field=col:integer", "conway", 'memory')
QgsProject.instance().addMapLayer(layer)
layer.renderer().symbol().setColor(QColor("black"))
layer.renderer().symbol().symbolLayer(0).setStrokeStyle(0)
global q
q = QgsConway(30, 0, 5750000, 20000, 20000)
q.checkerboard()
q.shuffle(3)
q.show()
act = QAction("step")
act.triggered.connect(q.one)
tb = iface.mainWindow().addToolBar("conway")
tb.addAction(act)
more = QAction("+")
more.triggered.connect(q.more)
tb.addAction(more)
cla = QAction("X")
timer = QTimer()
timer.timeout.connect(q.one)
timer.start(200)
q.timer = timer
stop = QAction("stop")
stop.triggered.connect(timer.stop)
tb.addAction(stop)
start = QAction("start")
start.triggered.connect(lambda: timer.start(200))
tb.addAction(start)
def clean():
act.deleteLater()
cla.deleteLater()
more.deleteLater()
stop.deleteLater()
start.deleteLater()
layer.deleteLater()
tb.deleteLater()
timer.stop()
timer.deleteLater()
q.clean = None
q.timer = None
q.tb = None
del qgis.utils.plugins['conway']
cla.triggered.connect(clean)
tb.addAction(cla)
q.tb = tb
q.clean = clean
qgis.utils.plugins['conway'] = q
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment