Hashlife in 53 lines of Lua

For scripts to aid with computation or simulation in cellular automata.
Post Reply
User avatar
blah
Posts: 311
Joined: April 9th, 2016, 7:22 pm

Hashlife in 53 lines of Lua

Post by blah » July 10th, 2020, 12:45 am

Hopefully this helps to demystify hashlife. It generates the "RESULT" of an r-pentomino surrounded by some empty space and prints it. See also this explanation. The program is divided into two halves, the hashlife implementation itself, and the part that interfaces with it to feed it a pattern and rule and print the result. I believe lines 3-55 constitute a complete hashlife implementation, hence the title of this thread.

Code: Select all

----------------------------------------------------------------------- HASHLIFE

nodes = {} -- table of nodes (whether in use or not)

-- the 4 quadrants of a node are labeled as such:
-- a b
-- c d

function key(n)
    return tostring(n.a)..tostring(n.b)..tostring(n.c)..tostring(n.d)
end

function isLeaf(n)
    return type(n.a) == "number"
end

function node(a,b,c,d)
    local newNode = {a=a,b=b,c=c,d=d}
    if nodes[key(newNode)] == nil then -- avoid duplicate nodes
        nodes[key(newNode)] = newNode
    end
    return nodes[key(newNode)]
end

function res(n)
    if n.res == nil then
        if isLeaf(n.a) then
            n.res = node( -- 2x2 inner result (where cell simulation happens)
            f(n.a.a,n.a.b,n.b.a, n.a.c,n.a.d,n.b.c, n.c.a,n.c.b,n.d.a),
            f(n.a.b,n.b.a,n.b.b, n.a.d,n.b.c,n.b.d, n.c.b,n.d.a,n.d.b),
            f(n.a.c,n.a.d,n.b.c, n.c.a,n.c.b,n.d.a, n.c.c,n.c.d,n.d.c),
            f(n.a.d,n.b.c,n.b.d, n.c.b,n.d.a,n.d.b, n.c.d,n.d.c,n.d.d))
        else-- Generate 3x3 of smaller nodes
            -- ABC
            -- DEF
            -- GHI
            local A = res(n.a)
            local B = res(node(n.a.b,n.b.a,n.a.d,n.b.c))
            local C = res(n.b)
            local D = res(node(n.a.c,n.a.d,n.c.a,n.c.b))
            local E = res(node(n.a.d,n.b.c,n.c.b,n.d.a))
            local F = res(node(n.b.c,n.b.d,n.d.a,n.d.b))
            local G = res(n.c)
            local H = res(node(n.c.b,n.d.a,n.c.d,n.d.c))
            local I = res(n.d)
            -- Generate final inner node out of them
            n.res = node(
            res(node(A,B,D,E)),
            res(node(B,C,E,F)),
            res(node(D,E,G,H)),
            res(node(E,F,H,I)))
        end
    end
    return n.res
end

---------------------------------------------------------------------- INTERFACE

function f(nw,n,ne,w,c,e,sw,s,se) -- game of life
    liveneighs = nw+n+ne+w+e+sw+s+se
    if liveneighs == 3 or (c == 1 and liveneighs == 2) then
        return 1
    else
        return 0
    end
end

function getcell(n,x,y,depth) -- coords for 4x4 go from -2 to 1
    if x<0 and y<0 then
        deeper = n.a
    elseif x>=0 and y<0 then
        deeper = n.b
    elseif x<0 and y>=0 then
        deeper = n.c
    elseif x>=0 and y>=0 then
        deeper = n.d
    end
    if isLeaf(n) then
        return deeper
    else
        local halfsize = 2^(depth-1)
        if x>=0 then newX = x-halfsize
        else         newX = x+halfsize end
        if y>=0 then newY = y-halfsize
        else         newY = y+halfsize end
        return getcell(deeper,newX,newY,depth-1)
    end
end

function printnode(n, depth)
    local size = 2^(depth+1)
    for y=0, size-1 do
        for x=0, size-1 do
            io.write(getcell(n,x-(size/2),y-(size/2),depth))
        end
        print("")
    end
end

rpent = node(  --....
node(0,0,1,1), --oo..
node(0,0,0,0), --.oo.
node(0,1,0,1), --.o..
node(1,0,0,0))
e2x2 = node(0,0,0,0)
e4x4 = node(e2x2,e2x2,e2x2,e2x2)
e8x8 = node(e4x4,e4x4,e4x4,e4x4)
e16x16 = node(e8x8,e8x8,e8x8,e8x8)
root = node(e16x16,e16x16,e16x16,
node(node(rpent,e4x4,e4x4,e4x4),e8x8,e8x8,e8x8))
printnode(res(root),3)
Here's the output:

Code: Select all

0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000011100000
0000000011110000
0000000110011000
0000000011010000
0000000011100000
0000000001000000
0000000000000000
0000000000000000
0000000000000000
Which is in fact generation 8 of the r-pentomino.

A few things to note:
  • There's no garbage collection.
  • Calling tostring() on a table apparently returns a unique identifier, like "table: 0x55ae98ee0890", so key(n) uniquely identifies n's contents (not n itself).
  • The mess of "n.a.b, n.b.a" looking nonsense in res() probably looks more complicated than it is. I don't expect you to read all of it; you don't have to, you just need to know what it's doing visually.
I wonder how small you could get a hashlife implementation if you didn't care about readability.
succ

User avatar
Pavgran
Posts: 220
Joined: June 12th, 2019, 12:14 pm

Re: Hashlife in 53 lines of Lua

Post by Pavgran » July 13th, 2020, 4:33 am

Well, HashLife isn't complicated at it's core. It's complicated at it's interface and practical application.
Here is an implementation I wrote for myself in Python:

Code: Select all

from __future__ import division, print_function

import itertools as it

from PyQt4 import QtCore as qtc
from PyQt4 import QtGui as qtg
import numpy


def b3s23(*cells):
    cnt = sum(cells)-cells[4]
    if cnt == 3:
        return True
    elif cnt == 2 and cells[4]:
        return True
    else:
        return False

class Cell(object):
    __slots__ = ['t']
    def __init__(self, *args):
        self.t = args
    def __getitem__(self, item):
        return self.t[item]

class Hashlife(object):
    __slots__ = ['dic', 'top', 'zero', 'level', 'rule']
    def __init__(self, rulef):
        self.reset()
        self.rule = tuple(rulef(*cells)\
            for cells in it.product((False, True), repeat=9))
    def reset(self):
        self.dic = {}
        self.top = None
        self.zero = None
        self.level = 0
        for c in it.product((False, True), repeat=4):
            cell = Cell(*c)
            self[c] = cell
    def __setitem__(self, key, value):
        if key in self.dic:
            raise ValueError("key already exists")
        self.dic[key] = value
    def __getitem__(self, key):
        try:
            return self.dic[key]
        except KeyError:
            try:
                c0 = key[0][4]
                c1 = self[key[0][1], key[1][0], key[0][3], key[1][2]][4]
                c2 = key[1][4]
                c3 = self[key[0][2], key[0][3], key[2][0], key[2][1]][4]
                c4 = self[key[0][3], key[1][2], key[2][1], key[3][0]][4]
                c5 = self[key[1][2], key[1][3], key[3][0], key[3][1]][4]
                c6 = key[2][4]
                c7 = self[key[2][1], key[3][0], key[2][3], key[3][2]][4]
                c8 = key[3][4]
                d0 = self[c0, c1, c3, c4][4]
                d1 = self[c1, c2, c4, c5][4]
                d2 = self[c3, c4, c6, c7][4]
                d3 = self[c4, c5, c7, c8][4]
                rs = self[d0, d1, d2, d3]
                cell = Cell(key[0], key[1], key[2], key[3], rs)
                self[key] = cell
                return cell
            except IndexError:
                celltup = (
                    (key[0][0], key[0][1], key[1][0], key[1][1]),
                    (key[0][2], key[0][3], key[1][2], key[1][3]),
                    (key[2][0], key[2][1], key[3][0], key[3][1]),
                    (key[2][2], key[2][3], key[3][2], key[3][3])
                )
                res = tuple(self.rule[sum(2**(8-ii)*jj\
                    for ii, jj in enumerate(celltup[i+di][j+dj]\
                        for di in (-1,0,1) for dj in (-1,0,1)))]\
                    for i in (1, 2) for j in (1, 2))
                cr = self[res]
                cell = Cell(key[0], key[1], key[2], key[3], cr)
                self[key] = cell
                return cell
    def toarr(self):
        dic = self.dic
        darr = list(dic.values())
        cellarr = [False, True]
        tuparr = [self.rule]
        start = 0
        while start < len(darr)+2:
            d = {j:i+start for i,j in enumerate(cellarr[start:])}
            if self.zero in d:
                zero = d[self.zero]
            if self.top in d:
                top = d[self.top]
            newcells = [i for i in darr if i[0] in d]
            newtups = [tuple(d[x] for x in i) for i in newcells]
            perm = sorted(range(len(newcells)), key=lambda x: newtups[x])
            if start > 0:
                tuparr += [newtups[i] for i in perm]
            start = len(cellarr)
            cellarr += [newcells[i] for i in perm]
        tuparr += [zero, top]
        return tuparr
    def fromarr(self, arr):
        self.rule = arr.pop(0)
        self.reset()
        arr = [False, True]\
            + [self[c] for c in it.product((False, True), repeat=4)]\
            + arr
        for i in range(18,len(arr)-2):
            arr[i] = Cell(*(arr[x] for x in arr[i]))
        self.zero = arr[arr[-2]]
        self.top = arr[arr[-1]]
        level = 0
        cur = self.top
        while type(cur) != bool:
            cur = cur[0]
            level += 1
        self.level = level
        d = {x[:4]:x for x in arr[2:-2]}
        self.dic = d
    def expand(self):
        c0 = self[self.zero, self.zero, self.zero, self.top[0]]
        c1 = self[self.zero, self.zero, self.top[1], self.zero]
        c2 = self[self.zero, self.top[2], self.zero, self.zero]
        c3 = self[self.top[3], self.zero, self.zero, self.zero]
        self.top = self[c0, c1, c2, c3]
        self.zero = self[self.zero, self.zero, self.zero, self.zero]
        self.level += 1
    def _compress(self, arr, start, stop, fromr=False, fromb=False):
        n, m = stop[0] - start[0], stop[1] - start[1]
        arr2 = [[None]*m for i in range(n)]
        for i in range(n):
            for j in range(m):
                arr2[i][j] = arr[i+start[0]][j+start[1]]
        arr = arr2
        level = 1
        zero = False
        while n > 1 or m > 1:
            n2m, m2m = n&1, m&1
            n2, m2 = (n+1)>>1, (m+1)>>1
            arr2 = [[None]*m2 for i in range(n2)]
            for i in range(n2):
                for j in range(m2):
                    c0 = arr[i*2-n2m*fromb][j*2-m2m*fromr] if\
                        i*2-n2m*fromb >= 0 and j*2-m2m*fromr >= 0 else zero
                    c1 = arr[i*2-n2m*fromb][j*2+1-m2m*fromr] if\
                        i*2-n2m*fromb >= 0 and j*2+1-m2m*fromr < m else zero
                    c2 = arr[i*2+1-n2m*fromb][j*2-m2m*fromr] if\
                        i*2+1-n2m*fromb < n and j*2-m2m*fromr >= 0 else zero
                    c3 = arr[i*2+1-n2m*fromb][j*2+1-m2m*fromr] if\
                        i*2+1-n2m*fromb < n and j*2+1-m2m*fromr < m else zero
                    arr2[i][j] = self[c0,c1,c2,c3]
            zero = self[zero, zero, zero, zero]
            n, m, arr = n2, m2, arr2
            level += 1
        return arr[0][0], level, zero
    def populate(self, arr):
        n, m = len(arr), len(arr[0])
        midn, midm = (n+1)>>1, m>>1
        c0, l0, z0 = self._compress(arr, (0, 0), (midn, midm), True, True)
        c1, l1, z1 = self._compress(arr, (0, midm), (midn, m), False, True)
        c2, l2, z2 = self._compress(arr, (midn, 0), (n, midm), True, False)
        c3, l3, z3 = self._compress(arr, (midn, midm), (n, m), False, False)
        level = l1
        zero = z1
        if l0 < level:
            c0 = self[z0, z0, z0, c0]
        if l2 < level:
            c2 = self[z2, c2, z2, z2]
        if l3 < level:
            c3 = self[c3, z3, z3, z3]
        self.level = level
        self.zero = zero
        self.top = self[c0, c1, c2, c3]
    def show(self, l, r, t, b, s, z):
        req = max(abs(x) for x in (l, r+1, t+1, b)) + s
        while req > 1<<(self.level-1):
            self.expand()
        if l > r:
            l, r = r, l
        if t < b:
            t, b = b, t
        s -= s%(1<<z)
        l -= s
        r += s
        t += s
        b -= s
        arr = [[self.top]]
        zero = [self.zero]
        n, m = 1, 1
        level = self.level
        cl = cb = -1<<(level-1)
        cr = ct = (1<<(level-1))-1
        cs = 0
        while level > z:
            ts = 1<<(level-1)
            cutl = l >= cl + ts
            cutr = r <= cr - ts
            cutt = t <= ct - ts
            cutb = b >= cb + ts
            cl += cutl*ts
            cr -= cutr*ts
            ct -= cutt*ts
            cb += cutb*ts
            n2 = n*2 - cutt - cutb
            m2 = m*2 - cutl - cutr
            arr2 = [[None]*m2 for i in range(n2)]
            for i in range(n2):
                for j in range(m2):
                    i2, di = divmod(i+cutt, 2)
                    j2, dj = divmod(j+cutl, 2)
                    arr2[i][j] = arr[i2][j2][2*di+dj]
            arr = arr2
            n, m = n2, m2
            ts = 1<<(level-2) if level > 1 else 1
            if s >= cs + ts:
                for i in range(n-1):
                    for j in range(m-1):
                        c0 = arr[i][j]
                        c1 = arr[i][j+1]
                        c2 = arr[i+1][j]
                        c3 = arr[i+1][j+1]
                        arr[i][j] = self[c0, c1, c2, c3][4]
                    arr[i].pop()
                arr.pop()
                n -= 1
                m -= 1
                cs += ts
            level -= 1
            zero = zero[0]
        if z > 0:
            for i in range(n):
                for j in range(m):
                    arr[i][j] = arr[i][j] != zero
        return arr
    def pprint(self, l, r, t, b, s, z):
        print('\n'.join(''.join([' ', '*'][x] for x in i)\
            for i in self.show(l, r, t, b, s, z)))

class MyWidget(qtg.QWidget):
    def __init__(self, hl):
        super(MyWidget, self).__init__()
        self.hl = hl
        self.pic = qtg.QLabel(self)
        self.size = qtc.QSize(400,400)
        self.pic.resize(self.size)
        self.timer = qtc.QTimer()
        self.cur = 0
    def render(self, l, r, t, b, s, z):
        arr = self.hl.show(l, r, t, b, s, z)
        w, h = len(arr[0]), len(arr)
        arr = numpy.packbits(numpy.array(arr, dtype=numpy.uint8), axis=1)
        img = qtg.QBitmap.fromData(
            qtc.QSize(w, h), 
            arr.data, qtg.QImage.Format_Mono)
        self.pic.setPixmap(img.scaled(self.size))
        self.show()
    def start(self, l, r, t, b, start, step, z, tstep):
        self.cur = start
        def tick():
            self.render(l, r, t, b, self.cur, z)
            self.cur += step
        self.timer.timeout.connect(tick)
        self.timer.start(tstep)
    def stop(self):
        self.timer.timeout.disconnect()
        self.timer.stop()

hl=Hashlife(b3s23)
rpent = [[False,True,True],[True,True,False],[False,True,False]]
acorn = [
    [False]+[True]+[False]*5,
    [False]*3+[True]+[False]*3,
    [True]*2+[False]*2+[True]*3
    ]
gg = [
    [False]*11+[True]*2+[False]*23,
    [False]*11+[True]*2+[False]*23,
    [True]*2+[False]*6+[True]*2+[False]*12+[True]*2+[False]*12,
    [True]*2+[False]*5+[True]*3+[False]*14+[True]+[False]*11,
    [False]*8+[True]*2+[False]*15+[True]+[False]*8+[True]*2,
    [False]*11+[True]*2+[False]*12+[True]+[False]*8+[True]*2,
    [False]*11+[True]*2+[False]*12+[True]+[False]*10,
    [False]*24+[True]+[False]*11,
    [False]*22+[True]*2+[False]*12
    ]
infgrow = [
    [False]*6+[True]+[False],
    [False]*4+[True]+[False]+[True]*2,
    [False]*4+[True]+[False]+[True]+[False],
    [False]*4+[True]+[False]*3,
    [False]*2+[True]+[False]*5,
    [True]+[False]+[True]+[False]*5
    ]
hl.populate(rpent)

a = qtg.QApplication([])
w = MyWidget(hl)
Core HashLife implementation is about 60 lines of code, and I've found that it was much simpler to write it than to write the function that extracted an arbitrary rectangle of the grid at arbitrary time step and zoom level (function show in the code above). And I've sidestepped issues of keeping memory bounded, rehashing, etc. Although I've wrote an interface between internal representation and an array (that could be dumped to a file, for example). And that also was not quite simple.

Edit:
Oh, and I don't think that your implementation of the core is complete in the sense that you handle empty tiles and universe expansion explicitly from outside. I think that work has to be done by the core itself, not by the interface or an external part.

User avatar
gameoflifemaniac
Posts: 1242
Joined: January 22nd, 2017, 11:17 am
Location: There too

Re: Hashlife in 53 lines of Lua

Post by gameoflifemaniac » July 13th, 2020, 5:20 am

Where can find an explanation of HashLife?
I was so socially awkward in the past and it will haunt me for the rest of my life.

Code: Select all

b4o25bo$o29bo$b3o3b3o2bob2o2bob2o2bo3bobo$4bobo3bob2o2bob2o2bobo3bobo$
4bobo3bobo5bo5bo3bobo$o3bobo3bobo5bo6b4o$b3o3b3o2bo5bo9bobo$24b4o!

User avatar
blah
Posts: 311
Joined: April 9th, 2016, 7:22 pm

Re: Hashlife in 53 lines of Lua

Post by blah » July 14th, 2020, 1:55 am

Pavgran wrote:
July 13th, 2020, 4:33 am
And I've sidestepped issues of keeping memory bounded, rehashing, etc.
What do you mean by 'rehashing'?
Pavgran wrote:
July 13th, 2020, 4:33 am
Edit:
Oh, and I don't think that your implementation of the core is complete in the sense that you handle empty tiles and universe expansion explicitly from outside. I think that work has to be done by the core itself, not by the interface or an external part.
Well, then here's a version that does that. 88 lines:

Code: Select all

----------------------------------------------------------------------- HASHLIFE

nodes = {} -- table of nodes (whether in use or not)

-- the 4 quadrants of a node are labeled as such:
-- a b
-- c d

function key(n)
	return tostring(n.a)..tostring(n.b)..tostring(n.c)..tostring(n.d)
end

function isLeaf(n)
	return type(n.a) == "number"
end

function node(a,b,c,d)
	local newNode = {a=a,b=b,c=c,d=d}
	if nodes[key(newNode)] == nil then -- avoid duplicate nodes
		nodes[key(newNode)] = newNode
	end
	return nodes[key(newNode)]
end

function depth(n) -- leaves have depth 0, 4x4s are depth 1, etc
	local depth = 0
	while not isLeaf(n) do
		depth=depth+1
		n = n.a
	end
	return depth
end

function res(n)
	if n.res == nil then
		if isLeaf(n.a) then
			n.res = node( -- 2x2 inner result (where cell simulation happens)
			f(n.a.a,n.a.b,n.b.a, n.a.c,n.a.d,n.b.c, n.c.a,n.c.b,n.d.a),
			f(n.a.b,n.b.a,n.b.b, n.a.d,n.b.c,n.b.d, n.c.b,n.d.a,n.d.b),
			f(n.a.c,n.a.d,n.b.c, n.c.a,n.c.b,n.d.a, n.c.c,n.c.d,n.d.c),
			f(n.a.d,n.b.c,n.b.d, n.c.b,n.d.a,n.d.b, n.c.d,n.d.c,n.d.d))
		else-- Generate 3x3 of smaller nodes
			-- ABC
			-- DEF
			-- GHI
			local A = res(n.a)
			local B = res(node(n.a.b,n.b.a,n.a.d,n.b.c))
			local C = res(n.b)
			local D = res(node(n.a.c,n.a.d,n.c.a,n.c.b))
			local E = res(node(n.a.d,n.b.c,n.c.b,n.d.a))
			local F = res(node(n.b.c,n.b.d,n.d.a,n.d.b))
			local G = res(n.c)
			local H = res(node(n.c.b,n.d.a,n.c.d,n.d.c))
			local I = res(n.d)
			-- Generate final inner node out of them
			n.res = node(
			res(node(A,B,D,E)),
			res(node(B,C,E,F)),
			res(node(D,E,G,H)),
			res(node(E,F,H,I)))
		end
	end
	return n.res
end

function empty(depth)
	local emptyNode = node(0,0,0,0)
	for i=1, depth do
		emptyNode = node(emptyNode,emptyNode,emptyNode,emptyNode)
	end
	return emptyNode
end

function runsim(n)
	local e = empty(depth(n))
	n = node(
	res(node(e,e,e,n)),
	res(node(e,e,n,e)),
	res(node(e,n,e,e)),
	res(node(n,e,e,e)))
	-- trim empty space
	e = empty(depth(n)-1)
	while n.a.a==e and n.a.b==e and n.a.c==e and
	      n.b.a==e and n.b.b==e and n.b.d==e and
	      n.c.a==e and n.c.c==e and n.c.d==e and
	      n.d.b==e and n.d.c==e and n.d.d==e do
		n = node(n.a.d,n.b.c,n.c.b,n.d.a)
	end
	return n
end

---------------------------------------------------------------------- INTERFACE

function f(nw,n,ne,w,c,e,sw,s,se) -- game of life
	liveneighs = nw+n+ne+w+e+sw+s+se
	if liveneighs == 3 or (c == 1 and liveneighs == 2) then
		return 1
	else
		return 0
	end
end

function getcell(n,x,y) -- coords for 4x4 go from -2 to 1
	if x<0 and y<0 then
		deeper = n.a
	elseif x>=0 and y<0 then
		deeper = n.b
	elseif x<0 and y>=0 then
		deeper = n.c
	elseif x>=0 and y>=0 then
		deeper = n.d
	end
	if isLeaf(n) then
		return deeper
	else
		local halfsize = 2^(depth(n)-1)
		if x>=0 then newX = x-halfsize
		else         newX = x+halfsize end
		if y>=0 then newY = y-halfsize
		else         newY = y+halfsize end
		return getcell(deeper,newX,newY)
	end
end

function printnode(n)
	local size = 2^(depth(n)+1)
	for y=0, size-1 do
		for x=0, size-1 do
			io.write(getcell(n,x-(size/2),y-(size/2)))
		end
		print("")
	end
end

root = node(   --....
node(0,0,1,1), --oo..
node(0,0,0,0), --.oo.
node(0,1,0,1), --.o..
node(1,0,0,0))
for i=1,3 do
	root = runsim(root)
end
printnode(root)
Although I actually disagree with you that this is necessary for a complete hashlife implementation. Consider a simulator for Wireworld or some other CA where the universe never actually expands, and so there's no code to automatically do that. If such a simulator isn't hashlife, then what is it?

If there's any argument to be made that it wasn't a complete hashlife implementation, it's that it doesn't do garbage collection.

Also, on a slightly unrelated note, I've noticed that nothing about the hashlife algorithm really requires an empty background. I don't see why you couldn't, with slight modification, have an implementation where patterns run on an infinite background of some (potentially oscillating) agar. If its spatial or temporal period isn't a power of 2 you could simply change the size of a leaf node to accommodate it more elegantly.
gameoflifemaniac wrote:
July 13th, 2020, 5:20 am
Where can find an explanation of HashLife?
Other than the one I already linked? This explanation by Tomas Rokicki is fairly comprehensive and straightforward.
succ

User avatar
Pavgran
Posts: 220
Joined: June 12th, 2019, 12:14 pm

Re: Hashlife in 53 lines of Lua

Post by Pavgran » July 16th, 2020, 5:02 am

blah wrote:
July 14th, 2020, 1:55 am
What do you mean by 'rehashing'?
I actually don't know exactly what it does and for what purpose, I've just found this term when looking through golly's hashlife source code.
blah wrote:
July 14th, 2020, 1:55 am
Although I actually disagree with you that this is necessary for a complete hashlife implementation. Consider a simulator for Wireworld or some other CA where the universe never actually expands, and so there's no code to automatically do that. If such a simulator isn't hashlife, then what is it?
Well, even if universe never expands, you still need to provide outer zeros so that hashlife, which knows nothing about the rule could actually be sure that there is no lightspeed signal from somewhere far away that disrupts the main patter after sufficiently large number of generations that you want to simulate.

Post Reply