Hi Paul, and welcome to the forums!
Thanks!
Your script causes an error in Golly 2.1 (and probably other versions).
Aye, there's the rub! RLE has lasted decades thanks to its independence from programming languages and Life implementations. And it is very easy to implement.
Here is my current Python (2.6) module for reading recursive RLE (and writing old-style RLE). The aforementioned "in place" transformation is effectively performed by the adjustment in the last line of _Reader()._xform(); note that it works relative to the origin of the subpattern's raster rather than the top-left corner of the its bounding rectangle, although these are the same if there are no blank rows or columns to the left of or above the subpattern, which I would describe as good style.
I have chosen here a native Python representation of Life patterns compatible with Golly, ie a flat list of co-ordinates [x0, y0, x1, y1, ...]. However, this module is independent of Golly. It would be a simple job to rewrite the module to work directly with Golly.
Code: Select all
import re
class _Writer(object):
def _addrun(self, n, c):
self.rle += (str(n) if n != 1 else "") + c
def _addlives(self):
if self._lives > 0:
self._addrun(self._lives, "o")
self._lives = 0
def __init__(self, cells):
self.rle = ""
self._lives = 0
points = [(cells[i + 1], cells[i]) for i in xrange(0, len(cells), 2)]
points.sort()
xc = yc = 0
for y, x in points:
if y > yc:
self._addlives()
self._addrun(y - yc, "$")
xc, yc = 0, y
if x > xc:
self._addlives()
self._addrun(x - xc, "b")
xc = x
self._lives += 1
xc += 1
self._addlives()
self.rle += "!"
class _Reader(object):
_REP = re.compile(r"""
( \d*[bo\$] # old-style run
| \@\w+ # name
| \+\d+ # evolve
| . ) # delimeter
""", re.VERBOSE)
_ID_CHAR = "."
_XFORM_CHARS = _ID_CHAR + r"<^>/-\|"
_TERMINATOR_CHARS = _XFORM_CHARS + "+"
_MATRICES = {
_ID_CHAR: ( 1, 0, 0, 1),
">": ( 0, 1, -1, 0),
"^": (-1, 0, 0, -1),
"<": ( 0, -1, 1, 0),
"|": (-1, 0, 0, 1),
"-": ( 1, 0, 0, -1),
"/": ( 0, -1, -1, 0),
"\\": ( 0, 1, 1, 0)
}
def __init__(self, str, evolve):
self._evolve = evolve
self._cache = {}
str = re.sub(r"\s+", "", str)
self._iterator = self._REP.finditer(str)
token, self.cells = self._parse(self._next(), "!")
def _next(self):
try: return self._iterator.next().group()
except StopIteration: return None
def _parse(self, token, terminators):
cells = []
x = y = 0
while token != None:
if token.startswith("@"):
subpattern = self._subpattern(token[1:])
cells.extend(self._xlate(subpattern, x, y))
elif token[-1:] in "bo$":
n = int(token[:-1]) if len(token) > 1 else 1
if token[-1:] == "$":
y += n
x = 0
else:
if token[-1:] == "o":
for i in xrange(n):
cells.extend([x + i, y])
x += n
else:
if token not in terminators:
raise SyntaxError, "Syntax error in rle"
return token, cells
token = self._next()
raise SyntaxError, "Syntax error in rle"
def _subpattern(self, name):
basekey = name + "+0" + self._ID_CHAR
token = self._next()
if token == "=":
token, self._cache[basekey] = self._parse(self._next(),
self._TERMINATOR_CHARS)
if token.startswith("+") and len(token) > 1:
n = int(token[1:])
token = self._next()
else:
n = 0
if token not in self._XFORM_CHARS:
raise SyntaxError, "Syntax error in rle"
key = name + "+" + str(n) + token
try:
return self._cache[key]
except KeyError:
pass
zerokey = name + "+0" + token
try:
result = self._cache[zerokey]
except KeyError:
result = self._cache[zerokey] = self._xform(self._cache[basekey],
token)
result = self._cache[key] = self._gen(result, n)
return result
def _xlate(self, cells, xt, yt):
if xt == yt == 0:
return cells
result = list(cells)
for i in xrange(0, len(result), 2):
x, y = result[i:i+2]
result[i:i+2] = x + xt, y + yt
return result
def _xform(self, cells, char):
if char == self._ID_CHAR:
return cells
matrix = self._MATRICES[char]
result = list(cells)
xm = ym = 0
for i in xrange(0, len(result), 2):
x, y = result[i:i+2]
xn, yn = (matrix[0] * x + matrix[1] * y,
matrix[2] * x + matrix[3] * y)
xm, ym = min(xm, xn), min(ym, yn)
result[i:i+2] = xn, yn
return self._xlate(result, max(0, -xm), max(0, -ym))
def _gen(self, cells, n):
if n == 0:
return cells
if self._evolve is None:
raise SyntaxError, "'+' used without supplied evolve function"
return self._evolve(cells, n)
def rle(cells):
return _Writer(cells).rle
def cells(str, evolve=None):
return _Reader(str, evolve).cells
__all__ = ["rle", "cells"]
if __name__ == "__main__":
from rlife import Pattern
def evolve(cells, n):
return Pattern(cells).evolved(n).cells
print cells("@G=3o$o$bo.10b@G+1^!", evolve)
I only started learning Python a week ago, so forgive - but also if you wish decry! - any bad style. I also tend to comment things only when they're "finished".
Note that evolve() is supplied as an argument to cells() in order for the module to be indepndent of any Life implementation available to the caller. In the test code at the end, rlife is a module which wraps RLife7.dll, a Life engine which I wrote years ago in C for use with my Glue Smalltalk program. It was a doddle to get it running without recompliation under Python (using the ctypes library).
I'm not sure scripts written in Python (or any other language) would always be the best way in which to express a complex construction. At the very least, some "scripts" might actually be organized by the originator as a collection of modules. I hope to start working with Herschel conduits in Python soon, and I would want to put their pattern definitions in a separate, reusable module - or even in a database. I am writing another module for defining a class of general affine transformations. A "script" to construct a large pattern from Herschel components would therefore rely on these modules, and probably many others, to run.
And then there is the method by which the script "outputs" its pattern. If it runs under Golly, it has one set of library calls to contend with. If it runs with some other life engine or program, it might need to use a completely different set.
On the other hand, if it outputs RLE, or any other Life pattern format whose implementation is relatively trivial, its output can be read by any program implementing that format. Python and Golly are not trivially implementable.
Or, to put it another way, I don't think any future implementation of Life should be required to provide a Golly-compatible Python-callable API so that they can "read" patterns published now as Python/Golly scripts.
Cheers, Paul