Golly scripts

For scripts to aid with computation or simulation in cellular automata.
User avatar
d/dx
Posts: 705
Joined: March 22nd, 2024, 2:41 pm
Location: in your walls

Re: Golly scripts

Post by d/dx » September 21st, 2024, 5:30 pm

List of things I did

Code: Select all

> go to python.org
> grab the python 3.12.6
> install normally
> no dlls anywhere
> uh oh
> look at dvgrn guide
> uninstall python
> reinstall as mr dave intended
> look in c drive
> folder labeled "python27"
> huh?
> i didnt install python 2.7
> oh look there's dlls
> oh no there's like 50 of them
> help
> hang on a sec
> look in program files
> folder labeled "python312"
> there it is
> open dlls folder
> double check with golly
> they're asking for "python3*.dll"
> ITS NOT THERE
> HELP IM SO CLOSE
my shtuffs

xkcd.com/626/


DieciFseis when?

,

User avatar
Tawal
Posts: 849
Joined: October 8th, 2023, 7:20 am
Location: Mælar

Re: Golly scripts

Post by Tawal » October 6th, 2024, 10:35 am

As this is not well explained in Golly's documentation and I found it myself, I'm sharing it here.

It can be interesting to import a personal python module with its own functions into a script.
The documentation explains that Golly starts a python script called golly-start.py when Golly starts.
Golly looks for this file in the same directory as the application executable.
So it's easy to import personal modules into Golly:
All you need to do is create this golly-start.py file (in the right location) with this kind of content:

Code: Select all

# Personal imports
from sys import path as syspath
syspath.append('/Absolute/Path/to/Folder/of/Personal_Modules')
And then inside a script, you just have to import your own modules like this :

Code: Select all

# Script for Golly
from My_Module import *
or other import forms.

"My_Module" is a My_Module.py file in the folder of personal modules.
Alone we go faster … Together we go further …
Avatar's pattern
My Sandbox
Bom-Bom
ℝ - ℕ = ℝ or ℕ ⊄ ℝ

User avatar
Tawal
Posts: 849
Joined: October 8th, 2023, 7:20 am
Location: Mælar

Re: Golly scripts

Post by Tawal » October 28th, 2024, 9:14 pm

I know : double post !
But 1 month ago …

I've written a function in Python3 that converts a clist into a rle string.
I know it already exists, but this one only scans cells in the clist (not all the bounding box).
It handles 2-states and multi-states clist.
Everything is done in a single stroke, a single scan of cells in the clist (except for list comprehensions).
It uses only min(), max(), len(), range(), sorted(), and chr().
I think it can be useful for large clist.

I called it clist2rle(), it takes only one argument : clist
Here's the code :

Code: Select all

def clist2rle(clist):
    rle = ""
    l = len(clist)
    t = 2 if l%2==0 else 3
    cl = [ clist[i:i+t] for i in range(0, l-1, t) ]
    cl = sorted([ [ c[1], c[0], *c[2:] ] for c in cl ])              # [ [y ,x], [y,x1], [y1,x2] … ]  y<y1, x<x1 … x2 can be smaller than x1
    x = min(clist[:-1:t])
    y = min(clist[1::t])
    s0 = "b" if t==2 else "."
    osc = ""
    ocx, ocy = x-1, y
    k = 1
    for c in cl:
        cy, cx, *st = c
        s = st[0] if st else 1
        sc = chr(64+s) if t==3 else "o"
        if cy>ocy:
            ocx = x-1
            if k>1:
                rle = f"{rle[:-1]}{k}{osc}"
                k = 1
            if cy-ocy>1:
                rle += str(cy-ocy)
            rle += "$"
            if cx-ocx>1:
                if cx-ocx>2:
                    rle += f"{cx-ocx-1}"
                rle += s0
            rle += sc
        else:
            if cx-ocx==1:
                if sc==osc:
                    k += 1
                else:
                    if k>1:
                        rle = f"{rle[:-1]}{k}{osc}"
                        k = 1
                    rle += sc
            else:
                if k>1:
                    rle = f"{rle[:-1]}{k}{osc}"
                    k = 1
                if cx-ocx>2:
                    rle = f"{rle[:-1]}{cx-ocx-1}{osc}"
                rle += s0 + sc
        ocx, ocy, osc = cx, cy, sc
    if k>1:
        rle = f"{rle[:-1]}{k}{rle[-1]}"
    return rle + "!"
Edit:
I can do without the "osc" variable (replaced by rle[-1], osc=old state character)
But this requires an additional test in the polling loop for the 1st round.
In terms of performance, I don't know whether a memory allocation takes precedence over a test,
but I do have the feeling that the test is faster even if it's repeated unnecessarily each round. (the affectation is also done each round …)


Edit May 24 2025:
There was an error on x position: corrected.
Last edited by Tawal on May 24th, 2025, 2:18 pm, edited 8 times in total.
Alone we go faster … Together we go further …
Avatar's pattern
My Sandbox
Bom-Bom
ℝ - ℕ = ℝ or ℕ ⊄ ℝ

User avatar
confocaloid
Posts: 6697
Joined: February 8th, 2022, 3:15 pm
Location: learn to protect yourself against stray gliders and sparks and self-destruct mechanisms

Re: Golly scripts

Post by confocaloid » October 28th, 2024, 9:36 pm

confocaloid wrote:
October 15th, 2024, 5:51 am
[...] However, that can be problematic in practice, because the user may need/want to use the system clipboard for another purpose at the same time. If possible, it is better to avoid writing to the clipboard, and even better to avoid using the clipboard at all.
Here is a simple Python 3 script to simplify switching between different rulesets.
The script attempts to interpret the clipboard content as a rulestring. IMO it is usually better to avoid using clipboard in scripts, but I decided to make an exception for this case.
I assigned the "backtick" keyboard shortcut for it (and assigned "Shift+backtick" for the usual dialog).
rulesetfromclipboard.py wrote:

Code: Select all

import golly as g

old_rulestring = g.getrule()
new_rulestring = g.getclipstr().strip()

if new_rulestring == "":
    g.show("empty clipboard")
else:
    try:
        g.setrule(new_rulestring)
    except Exception as e:
        g.show("could not interpret the clipboard content as a rulestring")
127:1 B3/S234c User:Confocal/R (isotropic CA, incomplete)
Unlikely events happen.
My silence does not imply agreement, nor indifference. If I disagreed with something in the past, then please do not construe my silence as something that could change that.

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » February 5th, 2025, 6:02 pm

Hello, I have a problem with a Golly script I have been working on which tries to implement the algorithm used in (w/r/j)lifesrc. It definitely looks like it is searching for something, but the output always seems to be incorrect. Also, for periods higher than one, the script completes nearly instantly without anything happening. Can anyone find what's wrong with this code and help me? I've been suffering with this for days.

Code: Select all

import math
import random
import golly as g
import numpy as np
from collections import deque
velx = 0
vely = 0

period = 1
width = 8
height = 8

neighbor = [(-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1)]

def tocells(n, x, y, z=0, w=0):
    c = []
    for q in np.argwhere(n>0.5):
        c.extend([q[0]+z,q[1]+w])
    return c

def toarray(c, x, y, z=0, w=0):
    n = np.zeros((x, y), dtype=np.int32)
    for i in range(0, len(c), 2):
        if 0 <= c[i]+z < x and 0 <= c[i+1]+w < y:
            n[c[i]+z, c[i+1]+w] = 1
    return n

def evolve(x, p, width=width, height=height):
    return toarray(g.evolve(tocells(x, width, height, 0, 0), p), width, height, 0, 0)

def display(r, x=0, y=0):
    if len(g.getrect()) != 0:
        g.select(g.getrect())
        g.clear(0)
        g.select([])
    g.putcells(g.transform(tocells(r, 0, 0), x, y))
    g.update()

cells = np.full((width, height, period), -1)
free = np.ones((width, height, period))
q = deque()
nextset = 0

def setcell(x, y, t, s, f):
    if cells[x, y, t] == s:
        return 1

    if cells[x, y, t] != -1:
        return 0

    cells[x, y, t] = s
    free[x, y, t] = f
    q.append([x, y, t, s, f])
    return 1

def consistify(x, y, t):
    t %= period
    prevcell = cells[x, y, t-1]
    nextprevcell = evolve(cells[:, :, t-1], 1)[x, y]
    if nextprevcell != cells[x, y, t]:
        if prevcell == 1:
            if cells[x, y, t] == 0:
                return 0
            cells[x, y, t] = 0
            free[x, y, t] = 0
            q.append([x, y, t, 1, 0])
        if prevcell == 0:
            if cells[x, y, t] == 1:
                return 0
            cells[x, y, t] = 1
            free[x, y, t] = 0
            q.append([x, y, t, 0, 0])
    if nextprevcell == 0 and cells[x, y, t] == 0 and setcell(x, y, t-1, 0, 0) != 1:
        return 0
    if nextprevcell == 1 and cells[x, y, t] == 1 and setcell(x, y, t-1, 1, 0) != 1:
        return 0
    s = -1
    if nextprevcell == 1 and cells[x, y, t] == -1:
        s = 0
    if nextprevcell == 0 and cells[x, y, t] == -1:
        s = 1
    if s == -1:
        return 1
    for i, j in neighbor:
        if 0 < x+i < width and 0 < y+j < height:
            if cells[x+i, y+j, t-1] == -1 and setcell(x+i, y+j, t-1, s, 0) != 1:
                return 0
    return 1

def consistify10(x, y, t):
    g.show(str(q))
    display(cells[:, :, 0])
    if consistify(x, y, t) != 1:
        return 0
    for i, j in neighbor:
        if 0 < x+i < width and 0 < y+j < height:
            if consistify(x+i, y+j, t+1) != 1:
                return 0
    return 1

def examinenext():
    if nextset == len(q):
        return 2
    cell = q.popleft()
    return consistify10(cell[0], cell[1], cell[2])

def proceed(x, y, t, s, f):
    if setcell(x, y, t, s, f) != 1:
        return 0
    while True:
        status = examinenext()
        if status == 0:
            return 0
        if status == 2:
            return 1

def backup():
    while len(q) != 0:
        cell = q.popleft()
        if cell[4] == 0:
            cells[cell[0], cell[1], cell[2]] = -1
            free[cell[0], cell[1], cell[2]] = 1
        else:
            nextset = len(q)
            return cell
    nextset = 0
    return "NULL"

def go(x, y, t, s, f):
    while True:
        status = proceed(x, y, t, s, f)
        if status == 1:
            return 1
        cell = backup()
        if cell == "NULL":
            return 0
        f = 0
        s = 1 - cells[x, y, t]
        cells[x, y, t] = -1

def getunknown():
    if len(np.argwhere(cells == -1)) != 0:
        return np.argwhere(cells == -1)[-1].tolist()
    return "NULL"

def search():
    cell = getunknown()
    if cell == "NULL":
        cell = backup()
        if cell == "NULL":
            return 0
        f = 0
        s = 1 - cell[3]
        cells[cell[0], cell[1], cell[2]] = -1
    else:
        f = 1
        s = 1
    while True:
        if go(cell[0], cell[1], cell[2], f, s) != 1:
            return 0

        cell = getunknown()
        if cell == "NULL":
            return 1
        f = 1
        s = 1

search()

User avatar
Tawal
Posts: 849
Joined: October 8th, 2023, 7:20 am
Location: Mælar

Re: Golly scripts : Stablise an unststable pattern

Post by Tawal » February 9th, 2025, 5:00 pm

Using 'mkstill' tool from 'bellman' (need compilation), I made a script to stabilise an unstable pattern in Golly.

How it works ?
In a multi-state universe, draw (copy or paste …) an unstable pattern.
Set in state 2 (blue cell in LifeHistory rule) the cells that are allowed to be ON to stabilise the pattern.
Then select your pattern and run the script.
It will make a file for mkstill tool.
If a solution is found, it will put it on the right of your initial pattern.
If not, a message tray is show up.
In case of an error of mkstill tool, it will open a window with the error message from mkstill.

It can be useful for welding.

Example :
I want to weld this white Snark with this green Speed Tunnel :

Code: Select all

x = 25, y = 55, rule = LifeHistory
15.C$13.3C$12.C$12.2C7$2.2C$.C.C5.2C$.C7.2C$2C2$14.C$10.2C.C.C$9.C.C.
C.C$6.C2.C.C.C.C.2C$6.4C.2C2.C2.C$10.C4.2C$8.C.C$8.2C.2A.2A$12.A.A$7.
2A.A5.A.2A$6.A.A.2A3.2A.A.A$5.A6.A.A6.A$5.7A3.7A2$7.3A7.3A$6.A2.A3.A
3.A2.A$6.2A4.A.A4.2A$12.A.A$13.A4$5.2A$5.2A5$.2A$.A5.2A5.2A$2.3A.A.A
4.A2.A$4.A.A6.A.A3.2A$5.2A7.A4.A.A.2A$10.2A9.A.A$11.A8.2A2.A$8.3A11.
2A$8.A11.2A$21.A$20.A$20.2A!
I draw a blue area between the two Still Life to stabilise (keeping the needing cells for the catalysts) :

Code: Select all

x = 25, y = 55, rule = LifeHistory
15.C$13.3C$12.C$12.2C7$2.2C$.C.C5.2C$.C7.2C$2C2$14.C$10.2C.C.C3B$9.C.
C.C.C3B$3.3BC2.C.C.C.C5B$3.3B4C.2C2.C5B$3.7BC10B$3.18B$3.18B$3.18B$7.
2A.A5BA.2A$6.A.A.2A3.2A.A.A$5.A6.A.A6.A$5.7A3.7A2$7.3A7.3A$6.A2.A3.A
3.A2.A$6.2A4.A.A4.2A$12.A.A$13.A4$5.2A$5.2A5$.2A$.A5.2A5.2A$2.3A.A.A
4.A2.A$4.A.A6.A.A3.2A$5.2A7.A4.A.A.2A$10.2A9.A.A$11.A8.2A2.A$8.3A11.
2A$8.A11.2A$21.A$20.A$20.2A!
I isolate them and select the result :

Code: Select all

x = 19, y = 19, rule = LifeHistory
11.C$7.2C.C.C3B$6.C.C.C.C3B$3BC2.C.C.C.C5B$3B4C.2C2.C5B$7BC10B$18B$
18B$18B$4.2A.A5BA.2A$3.A.A.2A3.2A.A.A$2.A6.A.A6.A$2.7A3.7A2$4.3A7.3A$
3.A2.A3.A3.A2.A$3.2A4.A.A4.2A$9.A.A$10.A!
Then I run the script on the selection above and get this result :

Code: Select all

x = 46, y = 19, rule = LifeHistory
11.C26.A$7.2C.C.C3B18.2A.A.A$6.C.C.C.C3B17.A.A.A.A$3BC2.C.C.C.C5B12.A
2.A.A.A.2A$3B4C.2C2.C5B12.4A.2A2.A$7BC10B16.A4.A$18B12.2A.A2.3A4.A$
18B12.A.2A.2A4.3A$18B16.A5.A$4.2A.A5BA.2A14.2A.A5.A.2A$3.A.A.2A3.2A.A
.A12.A.A.2A3.2A.A.A$2.A6.A.A6.A10.A6.A.A6.A$2.7A3.7A10.7A3.7A2$4.3A7.
3A14.3A7.3A$3.A2.A3.A3.A2.A12.A2.A3.A3.A2.A$3.2A4.A.A4.2A12.2A4.A.A4.
2A$9.A.A24.A.A$10.A26.A!
So, I reconstruct the initial pattern with the weld and test it :

Code: Select all

x = 33, y = 55, rule = LifeHistory
4.A10.A$5.A7.3A$3.3A6.A$12.2A7$2.2A$.A.A5.2A$.A7.2A$2A2$14.A$10.2A.A.
A$9.A.A.A.A$6.A2.A.A.A.2A$6.4A.2A2.A$10.A4.A$6.2A.A2.3A4.A$6.A.2A.2A
4.3A$10.A5.A$7.2A.A5.A.2A11.A$6.A.A.2A3.2A.A.A9.A$5.A6.A.A6.A8.3A$5.
7A3.7A2$7.3A7.3A$6.A2.A3.A3.A2.A$6.2A4.A.A4.2A$12.A.A$13.A4$5.2A$5.2A
5$.2A$.A5.2A5.2A$2.3A.A.A4.A2.A$4.A.A6.A.A3.2A$5.2A7.A4.A.A.2A$10.2A
9.A.A$11.A8.2A2.A$8.3A11.2A$8.A11.2A$21.A$20.A$20.2A!
Here is the script :

Code: Select all

# gmkstill.py    -    Golly Python 3 Script

# Make a Still Life from an unstable pattern in Golly.
#
# NEEDED:
#   - 'mkstill' from Bellman's suite :
#           https://sourceforge.net/projects/bellman
#   - Compilation needed before use.
#
# HOW TO:
#   In a LifeHistory universe,
#   (or multisate universe in case living cells have odd state) :
#       - Create a pattern to stabilise in any live state (1,3 or 5, …).
#       - Mark with state 2 (blue cell) cells which are allowed
#           to appear to stabilise the pattern.
#       - Select the pattern.
#       - Run the script.
#       - It will output :
#           + If there is a solution, Put the solution in state 1,
#               on the right of the selection +10 cells .
#           + If there's no solution, Show a message on the tray.
#           + If there's an error from 'mkstill', Open an window with
#               the error message.
#
# Authors :
#   Tawal (Pyhton) Feb. 09 2025
#   Mike Playle (mkstill) May 01 2013


######     HERE : Absolute path to 'mkstill' program (executable name included)       #######
mkstill = "/ABSOLUTE/PATH/TO/MKSTILL/EXECUTABLE"

######     HERE : Absolute path to temporary file for 'mkstill' (file name included)     #######
##                               You have to delete it as the script doesn't remove it.
tmp = "/ABSOLUTE/PATH/TO/TEMPORARY/FILE"




import golly as g
from subprocess import Popen, PIPE, STDOUT

# Can we run the script ?
numstates = g.numstates()
if numstates<3:
    g.exit("The script needs a multistate universe !")

r = g.getselrect()
if r==[]:
    r = g.getrect()
    if r==[]:
        g.exit("No pattern !")

# Get the input pattern.
pat = g.getcells(r)
l = len(pat)
c_pat = [ pat[i:i+3] for i in range(0, l-1, 3) ]

# Transform pattern for Bellman's mkstill program and write it in a file.
g.show(f"Creating file : {tmp} …")
with open(tmp, "w") as out:
    out.write("#P 0 0\n")
    for y in range(r[1],r[1]+r[3],1):
        for x in range(r[0],r[0]+r[2],1):
            for s in range(1, numstates, 2):
                if [x,y,s] in c_pat:
                    out.write("*")
                    break
            else:
                if [x,y,2] in c_pat:
                    out.write("?")
                else:
                    out.write(".")
        out.write("\n")

# Launch 'mkstil' on the created file.
g.show(f"Running 'mkstil' on {tmp} …")
proc = Popen([mkstill,tmp], stdout=PIPE, stderr=STDOUT, text=True, bufsize=1)

# Real time read output of 'mkstill'.
sl = ""
s = False
with proc.stdout:
    for l in iter(proc.stdout.readline, ''):
        if s:
            l = l.replace("*", "A")
            sl += f"{l}$"
        elif l[0]=="B":
            g.show(l)
        elif l[0]=="#":
            s = True

# Output the result.
code = proc.wait()
if code==0:
    if sl:
        g.putcells(g.parse(sl), r[0]+ r[2] + 10, r[1])
    else:
        g.show("No solution.")
else:
    e= ""
    with proc.stderr:
        for l in iter(proc.stderr.readline, ''):
            e += f"{l}\n"
    g.note(f"Error : \n{e}")
The script is also in attachment.
It needs to be modified in order to set the location of 'mkstill executable and the location for the temporary file.

I would advise you to run this script in another instance of Golly as 'mkstill' can take a long time and block your Golly instance while the script is running.
Attachments
gmkstill.zip
(1.46 KiB) Downloaded 91 times
Alone we go faster … Together we go further …
Avatar's pattern
My Sandbox
Bom-Bom
ℝ - ℕ = ℝ or ℕ ⊄ ℝ

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » February 9th, 2025, 5:38 pm

INT (or any 2-state range 1 rule) to MAP converter
This script will convert most rules into the MAP notation

Code: Select all

import base64
import golly as g
import numpy as np
from itertools import product

# NOTE: This should only work with 2-state range 1 rules.
# (i.e. B3/S23, B24/S12V, B2-a/S12)

def tocells(n):
    c = []
    for q in np.argwhere(n==1):
        c.extend([q[0],q[1]])
    return c

def toarray(c):
    n = np.zeros((3, 3))
    for i in range(0, len(c), 2):
        if 0 <= c[i] < 3 and 0 <= c[i+1] < 3:
            n[c[i], c[i+1]] = 1
    return n

def evolve(x, p):
    return toarray(g.evolve(tocells(x), p))

s = ""

for i, n in enumerate(product([0, 1], repeat=9)):
    n2 = evolve(np.array(n).reshape((3, 3)), 1)[1, 1]
    s += str(int(n2))

b = b""

for i in range(0, 512, 8):
    b += int(s[i:i+8], 2).to_bytes(1)

g.setrule("MAP" + base64.b64encode(b).decode("utf-8").strip("="))

User avatar
CARuler
Posts: 1337
Joined: July 30th, 2024, 5:38 pm
Location: A rule-verse in floor rule-verse of the CGOL skyscraper

thread for misc scripts

Post by CARuler » February 20th, 2025, 8:42 pm

i couldn't find a thread for miscellaneous scripts so this is one (mods: if one has already been made feel free to move it there)
a random fill script for golly:

Code: Select all

import golly as g
import random as r

def base10(l) :
    i = 0
    b = 0
    while (i < len(l)) :
        b = b+l[i]*(10**i)
        i = i+1
    return(b)
def STL(l) :
    a = []
    c = []
    i = 0
    while (i < len(l)) :
        if (l[i] == ",") :
            a.append(base10(c))
            c = []
            
        else :
            c.append(int(l[i]))
        if (i == len(l)-1) :
            a.append(base10(c))
        i = i+1
    return(a)

A = g.getstring("Enter the state(s) you want to use","")

B = STL(A)

C = g.getselrect()

D = g.getcells(C)

i = 0
while (3*i < len(D)) :
    g.setcell(D[3*i],D[3*i+1],r.choice(B))
    i = i+1


g.exit()
likes interesting rules
vist my rules here
also likes weird growth patterns in CA
hyperbolic CA!!!
ADHD user
mostly inactive

User avatar
confocaloid
Posts: 6697
Joined: February 8th, 2022, 3:15 pm
Location: learn to protect yourself against stray gliders and sparks and self-destruct mechanisms

Re: Golly scripts

Post by confocaloid » February 20th, 2025, 9:00 pm

CARuler wrote:
February 20th, 2025, 8:42 pm
i couldn't find a thread for miscellaneous scripts [...]
FYI, there are already at least these threads:
viewtopic.php?f=12&t=5313 Computer programs
viewtopic.php?f=9&t=3915 Thread for your script-related questions
viewtopic.php?f=9&t=2032 Script request thread
viewtopic.php?f=9&t=45 Golly scripts (the thread to which this was moved)
127:1 B3/S234c User:Confocal/R (isotropic CA, incomplete)
Unlikely events happen.
My silence does not imply agreement, nor indifference. If I disagreed with something in the past, then please do not construe my silence as something that could change that.

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » March 4th, 2025, 10:43 am

The quote below is my old message. Newer versions of the script are located at https://github.com/actlists/cgolscripts ... /golly2lls.
Here is a crappy Golly to LLS pattern script I made:

Code: Select all

import golly as g
r = g.getrect()
s = ""
p, tx, ty = g.getstring("""Enter period, x velocity, and y velocity (P,X,Y) - Examples:
4,1,1 - Glider
4,0,2 - XWSS
3,0,1 - c/3o
6,2,1 - (2,1)c/6""", "4,1,1").split(",")
p, tx, ty = int(p), int(tx), int(ty)
if r[2]//(p+1)*(p+1) != r[2]: g.exit("Pattern not divisible by period+1.")
for k in range(p+1):
    for i in range(r[3]):
        for j in range(r[2]//(p+1)):
            c = g.getcell(j+r[0]+r[2]//(p+1)*k, i+r[1])
            if c == 1:
                e = 1
            elif c == 2:
                e = 2
            elif c == 3:
                e = 3
            else:
                e = 0
            s += ["0,", "1,", "*,", f"v_{j+[0,tx][k==p]}_{i+[0,ty][k==p]},"][e]
        s += "\n"
    s += "\n"
with open(r"path/to/lls/pattern.txt", "w") as f:
    f.write(s)
Here is an example pattern to find c/2 barge wires (use 2,1,1 in the dialog):

Code: Select all

x = 99, y = 32, rule = LifeHistory
99F$F.A29.2F.A29.2F.A29.F$FA.A28.2FA.A28.2FA.A28.F$F.A.A27.2F.A.A27.
2F.A.A27.F$F2.A.A26.2F2.A.A26.2F2.A23C5.F$F3.A23C4.2F3.A23B4.2F3.23C
5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.
2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.
23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C
4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F
3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.
23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B
4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$
F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.23B4.2F3.23C5.F$F4.23C4.2F4.
23B4.2F3.23CA4.F$F4.23CA3.2F4.23BA3.2F25.A.A3.F$F26.A.A2.2F26.A.A2.2F
26.A.A2.F$F27.A.A.2F27.A.A.2F27.A.A.F$F28.A.A2F28.A.A2F28.A.AF$F29.A.
2F29.A.2F29.A.F$99F!
The border of state 6 cells is to make sure that the entire pattern is seen and parsed correctly.
State 2 cells are "*" cells, and state 3 cells are variables (which change depending on translation).
There are probably things I explained incorrectly about the program, if you have any issues let me know :D

User avatar
islptng
Posts: 475
Joined: May 24th, 2024, 6:17 am
Location: 种花家

Re: Golly scripts

Post by islptng » March 14th, 2025, 2:04 am

A script made to change self-complementary rules efficiently.

Code: Select all

def ConditionFormat(cstr):
	HENSEL = [['0'], ['1c', '1e'], ['2a', '2c', '2e', '2i', '2k', '2n'],
            ['3a', '3c', '3e', '3i', '3j', '3k', '3n', '3q', '3r', '3y'],
            ['4a', '4c', '4e', '4i', '4j', '4k', '4n', '4q', '4r', '4t', '4w', '4y', '4z'],
            ['5a', '5c', '5e', '5i', '5j', '5k', '5n', '5q', '5r', '5y'],
            ['6a', '6c', '6e', '6i', '6k', '6n'], ['7c', '7e'], ['8']]
	HenselInFun = [['0x'], ['1c', '1e'], ['2a', '2c', '2e', '2i', '2k', '2n'],
            ['3a', '3c', '3e', '3i', '3j', '3k', '3n', '3q', '3r', '3y'],
            ['4a', '4c', '4e', '4i', '4j', '4k', '4n', '4q', '4r', '4t', '4w', '4y', '4z'],
            ['5a', '5c', '5e', '5i', '5j', '5k', '5n', '5q', '5r', '5y'],
            ['6a', '6c', '6e', '6i', '6k', '6n'], ['7c', '7e'], ['8x']]
	cstr = cstr.replace("0","0x").replace("8","8x")
	res = ""
	currentnum = -1
	negated = False
	letterslist = []
	for supercalifragilisticexpialidocious in range(len(cstr) + 1):
		try:
			i = cstr[supercalifragilisticexpialidocious]
		except: i = "9"
		
		if i in "0123456789":
			if currentnum != -1:
				if len(letterslist) == 0:
					letterslist = [x[1] for x in HENSEL[currentnum]]
				if negated:
					newlist = []
					for j in HENSEL[currentnum]:
						if j[1] not in letterslist:
							newlist.append(j[1])
					letterslist = newlist
				for j in letterslist:
					res += str(currentnum) + j
			currentnum = int(i)
			negated = False
			letterslist = []
		elif i == "-": negated = True
		else: letterslist.append(i)
	return res


import golly as g

ld4 = {}
for i in range(13): ld4["cekainyqjrtwz"[i]] = "eckatrjwyniqz"[i]
def invertt(t):
	if t[0] == "4": return "4" + ld4[t[1]]
	fd = str(8 - int(t[0]))
	return fd + t[1]

xB,xS = g.getrule().split("/")
xB = ConditionFormat(xB[1:])
xS = ConditionFormat(xS[1:])
T = g.getstring("Enter the change you want to make (Birth):","","Self-complementary Maker")
survival = False
if T[0] == "S" or T[0] == "s":
	T = T[1:]
	survival = True
T = ConditionFormat(T)

if not survival:
	for i in range(len(T)//2):
		if T[2*i:2*(i+1)] in xB: xB = xB.replace(T[2*i:2*(i+1)],"")
		else: xB += T[2*i:2*(i+1)]
	xS = ConditionFormat("012345678")
	for i in range(len(xB)//2):
		xS = xS.replace(invertt(xB[2*i:2*(i+1)]),"")
else:
	for i in range(len(T)//2):
		if T[2*i:2*(i+1)] in xS: xS = xS.replace(T[2*i:2*(i+1)],"")
		else: xS += T[2*i:2*(i+1)]
	xB = ConditionFormat("012345678")
	for i in range(len(xS)//2):
		xB = xB.replace(invertt(xS[2*i:2*(i+1)]),"")

xB = xB.replace("x","")
xS = xS.replace("x","")
g.setrule(f"B{xB}/S{xS}")
My sandbox | All my engineered replicators | TNT
Asperger, ISTP, using a Dvorak keyboard.

On March 2nd, I'll begin my time travel to July (at least on the Internet). During these time, I do not exist, so don't try to contact me. (Not a joke!)

haxton1
Posts: 1
Joined: March 17th, 2025, 12:12 pm

Re: Golly scripts

Post by haxton1 » March 17th, 2025, 12:19 pm

Spilled from Discord.
The zip file also contains Fabio Crameri's Scientific Colour Maps, defined in a 3x256 .txt files.
You also can define your own colormaps using similar format:
[Red value] [Green value] [Blue value], all of them between 0 and 1, and separated by a space.
Included are some examples of the script. Note that the script only works on a generations rule (for now)

ImageImageImageImageImageImage

2025 Mar 18 00:40 UTC changed the example gifs (again)

Code: Select all

#   Code written by JY Park
#   an attempt to implement Crameri colormaps into Golly. 
#   only works on Generation rules?
#   www.fabiocrameri.ch/colourmaps Copyright (c) 2023, Fabio Crameri

import golly as g
import math

rule = g.getrule().split(":")
rule = rule[0].split("/")
if len(rule) == 2:
    states = 2
else:
    states = int(rule[2])

states = states - 1

name = g.getstring("Enter colormap name.")
name = name.lower()

cmap = []
with open(r"Colormap/"+name+".txt", "r") as fp:
    lines = 0
    List = fp.read().split("\n")
    for i in List:
        if i:
            lines +=1
            temp = i.split(" ")
            for j in range(3):
                temp[j] = float(temp[j])
            cmap.append(temp)

    dist = (lines-1)/states
    newcolors = []
    for i in range(states):
        k = i + 1
        loc = lines - dist*(k-0.5)
        ind = math.floor(loc)
        ratio = loc - ind

        newcolors.append(k)
        for j in range(3):
            newcolors.append(round((cmap[ind-1][j]*ratio + cmap[ind][j]*(1-ratio))*255))

    g.setcolors(newcolors)
Attachments
Crameri_Colormaps_in_Golly.zip
(1.36 MiB) Downloaded 73 times

User avatar
islptng
Posts: 475
Joined: May 24th, 2024, 6:17 am
Location: 种花家

Re: Golly scripts

Post by islptng » March 20th, 2025, 5:39 am

A tiny script to emulate "[[ RANDOMIZE2 ]]" in Golly:

Code: Select all

import golly as g
from random import randint as rand
try:
	x, y, w, h = g.getselrect()
except: g.exit("There is no selection.")

s = g.getoption("drawingstate")

for i in range(x,x+w):
	for j in range(y,y+h):
		g.setcell(i,j,rand(0,1)*s)
It is suggested to attach it to "Ctrl+Shift+5" (since Golly's default randomize key is Ctrl+5). Very useful when running R2INT emulators.
My sandbox | All my engineered replicators | TNT
Asperger, ISTP, using a Dvorak keyboard.

On March 2nd, I'll begin my time travel to July (at least on the Internet). During these time, I do not exist, so don't try to contact me. (Not a joke!)

User avatar
Tawal
Posts: 849
Joined: October 8th, 2023, 7:20 am
Location: Mælar

String to Pattern to paste.

Post by Tawal » May 11th, 2025, 4:18 pm

Here's a python3 script that converts an input string into an RLE string (giving a typo file) and copies it to the clipboard.
All that's left to do is paste the result.
All is explain in the comments.
You will find a typo file in attachment and also the script file.

Code: Select all

# String to RLE    -   Golly Python3 Script
#
# Need a typo-file 'character rle'.
#   - File Line's Format :
#        character [space] rle_string
#
# Author:   Tawal   -   2025-05-11

# How To:
#   - In Golly application, run the script.
#   - Enter the string to convert.
#   - The RLE string is copied on the clipboard.
#   - You just have to paste it where you want.


######    HERE THE ABSOLUTE PATH OF THE TYPO-FILE    ######
#

typo = "ABSOLUTE PATH TO TYPO FILE"

#
######


### Functions
def gfont(f):                              # Return a dict {char:rle,…}
    font = {}
    with open(f, "r") as _f:
        for l in _f:
            font[l[0]] = l.split(" ")[1]
    _f.close
    return font

def str2clist(s, font):                   # Return a clist from string and font.
    dx = 0
    pat = []
    oc = ""
    for c in s:
        if c == " ":
            dx += 7
            continue
        if (c or oc)=="1":
            dx += 1
        c_rle = font[c]
        c_cl = g.parse(c_rle)
        c_w = max([ i for i in c_cl[0::3] ])
        c_cl = g.transform(c_cl, dx, 0)
        pat = g.join(pat, c_cl)
        dx += c_w + 2
        oc = c
    return pat

def clist2rle(clist):                       # Return a RLE string from a clist (multistate implemented).
    rle = ""
    l = len(clist)
    t = 2 if l%2==0 else 3
    cl = [ clist[i:i+t] for i in range(0, l-1, t) ]
    cl = sorted([ (c[1], c[0], *c[2:]) for c in cl ])              # [ (y ,x), (y,x1), (y1,x2) … ]  y<y1, x<x1 … x2 can be smaller than x
    x = min(clist[:-1:t])
    y = min(clist[1::t])
    s0 = "b" if t==2 else "."
    osc = ""
    ocx, ocy = x-1, y
    k = 1
    for c in cl:
        cy, cx, *st = c
        s = st[0] if st else 1
        sc = chr(64+s) if t==3 else "o"
        if cy>ocy:
            ocx = x-1
            if k>1:
                rle = f"{rle[:-1]}{k}{osc}"
                k = 1
            if cy-ocy>1:
                rle += str(cy-ocy)
            rle += "$"
            if cx-ocx>1:
                if cx-ocx>2:
                    rle += f"{cx-ocx-1}"
                rle += s0
            rle += sc
        else:
            if cx-ocx==1:
                if sc==osc:
                    k += 1
                else:
                    if k>1:
                        rle = f"{rle[:-1]}{k}{osc}"
                        k = 1
                    rle += sc
            else:
                if k>1:
                    rle = f"{rle[:-1]}{k}{osc}"
                    k = 1
                if cx-ocx>1:
                    if cx-ocx>2:
                        rle += f"{cx-ocx-1}"
                    rle += s0
                rle += sc
        ocx, ocy, osc = cx, cy, sc
    if k>1:
        rle = f"{rle[:-1]}{k}{rle[-1]}"
    return rle + "!"


### Main
if __name__=="builtins":
    import golly as g

    s = g.getstring("Enter the string :", "", "String to RLE")
    s_cl = str2clist( s, gfont(typo) )
    g.setclipstr(f"x=0,y=0,rule=LifeHistory\n{clist2rle(s_cl)}")


Edit May 24 20205:
Corrected an error on clist2rle function (x position).
Attachments
typo_freywa.txt
(2.94 KiB) Downloaded 66 times
Alone we go faster … Together we go further …
Avatar's pattern
My Sandbox
Bom-Bom
ℝ - ℕ = ℝ or ℕ ⊄ ℝ

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » June 15th, 2025, 4:17 pm

Gollf - A very basic rule golfing utility for Golly

Randomly flips rule transitions and finds rules that are the most "similar" (by density, growth rate, and cell changes over time).
I have no clue what else to say other than it finds some pretty interesting CGOL-like rules occasionally. There's no explosion prevention, either.

Code: Select all

import random
import golly as g
import re
import copy
import numpy as np

flip_range = (2, 8)
score_diff = 0.05
rect = [0, 0, 64, 64]
steps = 50

hensel1 = {
    0: "",
    1: "ce",
    2: "acekin",
    3: "acekijnqry",
    4: "acekijnqrtwyz",
    5: "acekijnqry",
    6: "acekin",
    7: "ce",
    8: ""
}

hensel2 = ['0', '1c', '1e', '2a', '2c', '2e', '2k', '2i', '2n', '3a', '3c', '3e', '3k', '3i', '3j', '3n', '3q', '3r', '3y', '4a', '4c', '4e', '4k', '4i', '4j', '4n', '4q', '4r', '4t', '4w', '4y', '4z', '5a', '5c', '5e', '5k', '5i', '5j', '5n', '5q', '5r', '5y', '6a', '6c', '6e', '6k', '6i', '6n', '7c', '7e', '8']

rule = g.getrule()

def fix_hensel(num):
    if len(num) == 1: return num + hensel1[int(num)]
    if len(num.split("-")) == 1: return num
    o, h = num.split("-")
    h = "".join(sorted(h))
    n = ""
    for s in hensel1[int(o)]:
        if s not in h:
            n += s
    return o + n

def snapshot(r):
    a = np.zeros((r[2], r[3]))
    for i in range(r[0], r[0]+r[2]):
        for j in range(r[1], r[1]+r[3]):
            a[i-r[0], j-r[3]] = g.getcell(i, j)
    return a

def expand_hensel(rule):
    rb = rule[1:].split("/S")[0]
    rs = rule.split("/S")[1]
    rb1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rb))]
    rs1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rs))]
    rb2 = []
    for r in rb1:
        if len(r) == 1:
            rb2.append(r)
        for rr in r[1:]:
            rb2.append(r[0] + rr)
    rs2 = []
    for r in rs1:
        if len(r) == 1:
            rs2.append(r)
        for rr in r[1:]:
            rs2.append(r[0] + rr)
    fin = []
    for h in hensel2:
        if h not in rb2:
            fin.append(0)
        else:
            fin.append(1)
    for h in hensel2:
        if h not in rs2:
            fin.append(0)
        else:
            fin.append(1)
    return fin

def random_flip(bits):
    bits2 = copy.deepcopy(bits)
    r = random.randint(0, len(bits2)-1)
    bits2[r] = 1 - bits2[r]
    return bits2

def rule_from_bits(bits):
    ss = "B"
    for b in range(len(hensel2)):
        if bits[b]:
            ss += hensel2[b]
    ss += "/S"
    for s in range(len(hensel2)):
        if bits[len(hensel2) + s]:
            ss += hensel2[s]
    return ss

def calculate_growth_rates(d):
    growth_rates = [0]
    for i in range(1, len(d)):
        previous_value = d[i - 1]
        current_value = d[i]
        if previous_value != 0:
          growth_rate = (current_value - previous_value) / previous_value
          growth_rates.append(growth_rate)
        else:
          growth_rates.append(0)
    return growth_rates

def interestingness_score(history):
    changes = []
    densities = []
    growths = []

    for t in range(1, len(history)):
        prev = history[t-1]
        curr = history[t]
        changes.append(np.mean(prev != curr))
        densities.append(np.mean(curr))
        growths.append(np.mean(curr))
    growths = calculate_growth_rates(growths)
    
    avg_change = np.mean(changes)
    avg_density = np.mean(densities)
    avg_growths = np.mean(growths)

    g.show(str((avg_change, avg_density, avg_growths)))

    return avg_change, avg_density, avg_growths
g.new('untitled')
hist = []
g.select(rect)
g.clear(0)
g.clear(1)
g.randfill(50)
for t in range(steps):
    g.run(1)
    hist.append(snapshot(rect))

score0 = np.array(interestingness_score(hist))
with open("golf.txt", "a") as f:
    f.write(f"\nOriginal Rule: {rule}; Flipping range: {flip_range}; Fill rect: {rect}; Steps for history: {steps}\n")
while True:
    hist = []
    bits = expand_hensel(rule)
    for i in range(*flip_range):
        bits = random_flip(bits)
    g.clear(0)
    g.clear(1)
    g.randfill(50)
    g.setrule(rule_from_bits(bits))
    for t in range(steps):
        g.run(1)
        hist.append(snapshot(rect))
    score1 = np.array(interestingness_score(hist))
    g.update()
    if (np.abs(score0 - score1) < score_diff).all():
        with open("golf.txt", "a") as f:
            f.write(f"Rule: {g.getrule()}; Difference: {np.mean(np.abs(score0 - score1))}\n")

User avatar
Tawal
Posts: 849
Joined: October 8th, 2023, 7:20 am
Location: Mælar

Re: Golly scripts

Post by Tawal » July 15th, 2025, 1:42 am

EDIT Jul. 21: The script is now working. With brutal force method not dichotomy.

I asked if there is an interest to compress Single Lane Channel (i.e. reduce the separation between Gliders).
dvgrn answered : viewtopic.php?p=214575#p214575

So, I made a Golly Python 3 script to do that.
And it runs not too slow on my mind.

How to do :
The Single Lane Channel must be NW directed and first Glider must be :

Code: Select all

x = 3, y = 3, rule = B3/S23
b2o$2o$2bo!
The Seed must be outside the total reaction's envelop.
Then select the Seed and run the script.
You will be prompted to enter the compression expected (in ticks).
That's all.


Here is the script :

Code: Select all

# Compress Single Lane Channel  -  Golly Python3 Script
#
# Restrictions;
#   - Single Lane Channel must be directed to NW.
#   - First Glider must to be b2o$2o$2bo!
#   - First Glider must to be outside the total reaction's enveloppe.
#   - 2 states universe only (i.e, rule = B3/S23).
#
# How To:
#   - On a new layer, put your Single Lane Channel with the Seed(s) as the stream is directed to NW.
#   - Select the Seed(s) without selecting any cell of any Glider.
#   - Run the script.
#           This may take a while belong the length of Single Lane Channel.
#           The script indicates its progression (i.e., Current processing Glider, Total Compression in ticks).
#
# Author : Tawal 2025-07-21

###   Imports
import golly as g
import re


# NW Gliders                                binaries       decimal
GLIDERS = [                 # Table :     XXX    parity     index   -  What is XXX ? It's not 'colour' neither 'phase'
    g.parse("b2o$2o$2bo!"), #              0       0          0
    g.parse("3o$o$bo!"),    #              0       1          1
    g.parse("bo$2o$obo!"),  #              1       0          2
    g.parse("2o$obo$o!")    #              1       1          3
]

###    Functions
# clist functions
def clist2rect(clist):
    x, y = min(clist[::2]), min(clist[1::2])
    w, h = max(clist[::2])-x+1, max(clist[1::2])-y+1
    return [x, y, w ,h]

def canon_clist(clist):
    out = []
    x, y, w, h = clist2rect(clist)
    for i in range(0, len(clist), 2):
        out += [clist[i]-x, clist[i+1]-y]
    return out

# Gliders functions
def mv_glid(glid, ticks):                              # Call new_glid() and make change on layer
    ng = new_glid(glid, ticks)
    g.select([glid[1], glid[2], 3, 3])
    g.clear(0)
    g.putcells(GLIDERS[ng[0]], ng[1], ng[2])
    return ng

def new_glid(glid, ticks):                             # Can be simplified - Surely !
    ind0, x0, y0 = glid
    ind = (ind0 + ticks)%4
    st = 1 if ticks>0 else -1
    shift = (st*ticks)//4 * st                         # Minimal diagonal shift
    adj_shift = 1 if (-st*ticks)%4==1 else 0           # Adjust the shifting
    ph = (ind - ind%2)//2                              # Called 'ph' for 'phase' but what it is ? See GLIDERS definitions
    dph = ph - (ind0 - ind0%2)//2
    dx = st*( dph*((dph-st)//2) + adj_shift*(1-abs(dph)) )
    dy = st*( dph*((dph+st)//2) + adj_shift*(1-abs(dph)) )
    x = x0 - (shift + dx)
    y = y0 - (shift + dy)
    return ind, x, y                                   # ind in GLIDERS, x, y

def find_glids():                                      # Special Glider finder for Single Lane only
    coords = []
    n = 0
    jump = 0
    x, y, w, h = g.getrect()
    for dy in range(h-2):
        if jump:
            jump -= 1
            continue
        pat = g.getcells([x+dy, y+dy, 4, 4])
        if pat:
            px, py, *_ = clist2rect(pat)
            pat = canon_clist(pat)
            if pat in GLIDERS:
                if not (n or pat==GLIDERS[0]):
                        return 'BAD_FIRST'
                ind = GLIDERS.index(pat)
                coords += [ (ind, px, py) ]
                jump = 4
                n += 1
                g.show(f'{n} Gliders found')
        else:
            jump = 2
    return coords

# Stream Compression functions
def stream_chunk(stream, start, n):                    # Generator: G_start-n+1, G_start-n+2 …, G_start , Ind_G_start-n+1
    l = len(stream)
    for i in range(start, l, 1):
        out = [ stream[k] for k in range(i-n+1, i+1, 1) ]
        out += [i-n+1]
        yield out

def is_ok(g1, g2, g3, ticks):
    # First evolve
    r = g.getrect()
    run = max(r[2], r[3])*4
    p = g.getcells(r)
    ash = g.evolve(p, run)
    # Move Gliders and evolve
    ng1 = mv_glid(g1, ticks)
    ng2 = mv_glid(g2, ticks)
    ng3 = mv_glid(g3, ticks)
    p = g.getcells(r)
    ash1 = g.evolve(p, run-ticks)
    # Restore Gliders on layer
    ng3 = mv_glid(ng3, -ticks)
    ng2 = mv_glid(ng2, -ticks)
    ng1 = mv_glid(ng1, -ticks)
    if ash==ash1:
        return True
    return False




###   Main
if __name__=='builtins':
    r_seed = g.getselrect()
    if not r_seed:
        g.exit('Pas de sélection ! Exit.')
    else:
        g.shrink()
        r_seed = g.getselrect()
        seed = g.getcells(r_seed)
        if not seed:
            g.exit('Sélection vide ! Exit.')
        g.clear(0)                                     # Clear the seed to well scan on diagonal for Gliders.
                                                       # Total comparisons =~  h_stream//2 - nb_Gliders

    # User choice : Minimal distance expected between 2 Gliders (ticks)
    valid = None
    reg = re.compile(r'(?P<dist>^\d+$)')
    title = 'Single Lane Channel Compressor'
    prompt = '''If you CANCEL this box, reset the pattern to retrieve the seed.
               Press 'f' to update progression's viewing.

          Distance expected between 2 Gliders (in ticks) :'''
    while not valid:
        s = g.getstring(prompt, "61", title)
        valid = reg.match(s)
    dist = int(valid.group('dist'))
    dist = max(dist, 14)

    # Locate Gliders
    g.show('Searching for NW Gliders …')
    stream = find_glids()
    g.putcells(seed)                                   # Restore seed.
    if stream=='BAD_FIRST':
        g.note('First Glider is not b2o$2o$2bo!')
        g.exit(' ')

    seps = [0]                                         # Indexes shifted by 1 ==> seps[1] = Tg1 - Tg0
    chunks = stream_chunk(stream, 1, 2)                # g0, g1, ind0 … gN-1, gN, indN-1 …    (i.e. N = len(stream) = nb Gliders)
    for g1, g2, _ in chunks:
        seps += [(g2[1] - g1[1])*4 - (g2[0]-g1[0])]    # i.e. (X_g1 - X_g0)*4 - (ind_g1 - ind_g0)
    smin_in = min(seps[1:])
    smax_in = max(seps)

    # Start Compression
    start = g.getcells([r_seed[0], r_seed[1], stream[2][1]-r_seed[0]+3, stream[2][2]-r_seed[1]+3 ])  # Seed + 3 Gliders
    l1 = g.addlayer()
    g.new('SLC Compression Working Layer')
    g.putcells(start)
    nb_g = len(stream)

    ratio = nb_g//10                                   # Ratio to update the layer (user visualisation  ~ all 10% reached)
    n_gc = 0                                           # Nb of Gliders compressed/advanced
    N = 0
    tot = 0                                            # Total Compression in ticks
    gens = 0
    chunks = stream_chunk(stream, 3, 3)                # g1, g2, g3, ind1 … gN-2, gN-1, gN, indN-2 …
    for g1, g2, g3, ind in chunks:
        nu = (ind//ratio+1)*ratio
        nu = f' at Glider: {nu}' if nu<=nb_g else ': Finish'
        g.show(f'Processing Glider: {ind+1}/{nb_g}   -   Compression: {tot} ticks \t\t\t Next update{nu}')

        g1 = new_glid(g1, tot+gens)                    #  Put a Glider more and adjust the coordinates/type of triplet
        g2 = new_glid(g2, tot+gens)
        g.putcells(GLIDERS[g3[0]], g3[1], g3[2])
        g3 = mv_glid(g3, tot+gens)

        if ind%ratio==(ratio-1) or ind==1:             # Update layer for user ~ all 10% reached
            g.select([g1[1], g1[2], 3, 3])             # Seeing the processing Glider at this time
            g.fit()
            g.update()

        for i in range(seps[ind]-dist, 0, -1):         # Compress 1rst Glider of triplet (followed by the two last ones)
            if is_ok(g1, g2, g3, i):
                N = i
                break
        tot += N
        n_gc += 1 if N else 0

        g1 = mv_glid(g1, N)                            # Gliders on layer
        g2 = mv_glid(g2, N)
        g3 = mv_glid(g3, N)
        stream[ind] = new_glid(g1, -gens)              # 1rst Glider stored with modifications

        step = seps[ind] - N                           # Run the pattern for a step
        g.run(step)
        gens += step
        N = 0

    #Compress 2 last Gliders.
    g2 = new_glid(g2, step)
    g3 = new_glid(g3, step)
    for i in range(seps[-2]-dist, 0, -1):
        if is_ok(g2, g3, g3, i):
            N = i
            break
    tot += N
    stream[-2] = new_glid(stream[-2], tot)
    n_gc += 1 if N else 0

    g2 = mv_glid(g2, N)
    g3 = mv_glid(g3, N)
    N = 0
    for i in range(seps[-1]-dist, 0, -1):
        if is_ok(g3, g3, g3, i):
            N = i
            break
    tot += N
    stream[-1] = new_glid(stream[-1], tot)
    n_gc += 1 if N else 0

    # Output result
    g.new(f'SLC_Compressed_p{dist}_{tot}')
    g.putcells(seed)
    seps_out = []
    chunks = stream_chunk(stream, 1, 2)
    for g1, g2, _ in chunks:
        g.putcells(GLIDERS[g1[0]], g1[1], g1[2])
        seps_out += [(g2[1] - g1[1])*4 - (g2[0]-g1[0])]
    g.putcells(GLIDERS[g2[0]], g2[1], g2[2])
    smin_out = min(seps_out)
    smax_out = max(seps_out)
    g.fit()
    g.show(f'Compression: {tot}\
   -   Gliders compressed: {n_gc}/{nb_g}\
   -   Min-Max:  old: {smin_in}-{smax_in}, new: {smin_out}-{smax_out}')
How it works ?
Passing the details how the Gliders are found and stored,
I put the Seed and the 3 first Gliders of the stream on a new layer.
Then (in a loop), I put a Glider more.
So we have : The Seed and 4 Gliders.
Then I launch a recursive dichotomy to find the best compression between the very first Glider and the second. (edit: not in use, change by brutal force method)
Then I move the triplet of Gliders (3 last ones) on the layer (and store the right data of the Glider which have been compressed).
Then I run the pattern for a step (i.e the new separation between 1rst and 2nd Glider)
Etc …

Edit: Why I use a triplet of Gliders ?
First, I tried with only one Glider (+ the very first).
But it arrives sometimes that the next Glider doesn't pass correctly.
So, I tried with 2 Gliders. It was better but not perfect => It arrives sometimes that the last Glider is just clean destroyed without changing the reaction.
So, I use 3 Gliders. The 2 last ones are used to check if the reaction is right.


EDIT Jul. 21:
Dichotomy is not a good method: e.g., testing an advanced Glider at the middle can not work, but may be it can work with higher compression.
So the best method I found is to scan all possibilities from the maximum compression to zero.
It means that the script runs slower.
Doing that, I found another special case:
  • A glider can not to be advanced (i.e. keeping its original separation belong its previous)
  • Then the following Glider can be advanced
  • Once the following Glider is advanced, it is possible to advance the Glider which can't be before (i.e. second pass)
Example for illustration:
Compressing the SLC SnarkMaker for Spiral Growth.
At a time, I get this stage where the white Glider (and its 2 following Gliders in yellow) can't be advanced to reach 61 ticks or more:

Code: Select all

x = 314, y = 236, rule = LifeHistory
19.2A$20.A$18.A$18.5A15.A$23.A13.A.A$20.3A14.A.A$19.A18.A$19.4A$17.2A
3.A3.2A5.2A$16.A2.3A4.2A4.A2.A11.3A$16.2A.A13.2A$19.A25.A$19.2A24.A$
45.A$49.2A$48.A.A$48.2A34$74.A$74.A6.2A$74.A6.2A2.2A$85.2A4$93.3A$75.
2A2.2A$75.2A2.2A2$71.A$70.A.A7.2A$2A69.A7.3A$2A$79.A$73.3A3.2A$79.A7$
98.A$97.2A$97.A.A36$136.A$135.2A$135.A.A34$170.3A$170.A$171.A9$193.4D
$193.2D$193.D.D$193.D2.D$186.2A9.D13.3D3.3D23.D17.D2.3D2.5D8.3D3.3D9.
3D4.D3.D$185.2A11.D11.D3.D.D3.D7.D5.D8.D16.D2.D3.D5.D7.D3.D.D3.D7.D6.
2D4.D$187.A11.D10.D3.D.D3.D5.5D7.4D.D2.D3.4D5.D7.D4.D2.5D.D3.D.D3.D7.
D7.D5.D$200.D10.4D2.3D8.D5.D2.D5.D.D3.D9.D5.2D4.D10.4D2.3D2.5D.4D4.D
5.D$201.D12.D.D3.D7.D5.D2.D5.3D4.3D6.D7.D2.D4.5D5.D.D3.D7.D3.D3.D5.D$
202.D11.D.D3.D7.D5.D2.D5.D2.D6.D6.D2.D3.D.D15.D.D3.D7.D3.D3.D4.D$203.
D7.3D3.3D9.2D3.2D2.4D.D3.D.4D8.D2.3D2.D12.3D3.3D9.3D3.3D2.D$204.D$
205.D$206.D$207.D$208.D$209.D$210.D$211.D$212.D$213.D2.D$214.D.D$215.
2D$213.4D4$242.3D3.2D11.D24.D3.3D3.3D3.3D3.3D$211.C11.D17.D3.D3.D4.D
6.D23.D.D.D3.D.D3.D.D3.D.D$210.2C10.D2.5D.5D5.D7.D8.4D2.3D2.D.2D6.4D
3.D6.D.D2.2D5.D.D$210.C.C8.D19.D2.2D3.D4.D2.D3.D.D3.D.2D2.D5.D3.D8.D
2.D.D.D4.D2.4D$222.D2.5D.5D5.D3.D3.D4.D2.D3.D.5D.D9.D3.D7.D3.2D2.D3.D
3.D3.D$223.D17.D3.D3.D4.D2.D3.D.D5.D9.D3.D6.D4.D3.D2.D4.D3.D$242.3D4.
2D3.2D2.4D2.4D.D9.D3.D5.5D2.3D2.5D2.3D2$202.4D$202.2D$202.D.D$202.D2.
D$206.D$207.D$208.D$209.D$210.D$163.3D3.3D23.D15.D$162.D3.D.D3.D7.D5.
D8.D16.D$162.D3.D.D2.2D5.5D7.4D.D2.D3.4D7.D$163.4D.D.D.D7.D5.D2.D5.D.
D3.D12.D$166.D.2D2.D7.D5.D2.D5.3D4.3D10.D$166.D.D3.D7.D5.D2.D5.D2.D6.
D10.D$163.3D3.3D9.2D3.2D2.4D.D3.D.4D12.D$218.D14.2E$219.D12.2E$220.D
13.E$221.D$222.D2.D$223.D.D$224.2D$222.4D16$255.3E$255.E$256.E!
Glider n°2026 have a space of 37 ticks to reach p61. But no value between 37 and 0 works (i.e. the Glider can not be advanced)

A stage later, the following Glider can be advanced normally (edit: I didn't show the third Glider for this 2027th Glider advance)

Code: Select all

x = 314, y = 228, rule = LifeHistory
19.2A$20.A$18.A$18.5A15.A$23.A13.A.A$20.3A14.A.A$19.A18.A$19.4A$17.2A
3.A3.2A5.2A$16.A2.3A4.2A4.A2.A11.3A$16.2A.A13.2A$19.A25.A$19.2A24.A$
45.A$49.2A$48.A.A$48.2A34$74.A$74.A6.2A$74.A6.2A2.2A$85.2A4$93.3A$75.
2A2.2A$75.2A2.2A2$71.A$70.A.A7.2A$2A69.A7.3A$2A$79.A$73.3A3.2A$79.A7$
98.A$97.2A$97.A.A36$136.A$135.2A$135.A.A34$170.3A$170.A$171.A9$193.4D
$193.2D$193.D.D$193.D2.D$186.2A9.D13.3D3.3D23.D17.D2.3D2.5D8.3D3.3D9.
3D4.D3.D$185.2A11.D11.D3.D.D3.D7.D5.D8.D16.D2.D3.D5.D7.D3.D.D3.D7.D6.
2D4.D$187.A11.D10.D3.D.D3.D5.5D7.4D.D2.D3.4D5.D7.D4.D2.5D.D3.D.D3.D7.
D7.D5.D$200.D10.4D2.3D8.D5.D2.D5.D.D3.D9.D5.2D4.D10.4D2.3D2.5D.4D4.D
5.D$201.D12.D.D3.D7.D5.D2.D5.3D4.3D6.D7.D2.D4.5D5.D.D3.D7.D3.D3.D5.D$
202.D11.D.D3.D7.D5.D2.D5.D2.D6.D6.D2.D3.D.D15.D.D3.D7.D3.D3.D4.D$203.
D7.3D3.3D9.2D3.2D2.4D.D3.D.4D8.D2.3D2.D12.3D3.3D9.3D3.3D2.D$204.D$
205.D$206.D$207.D$208.D$209.D$210.D$211.D$212.D$213.D2.D$214.D.D$215.
2D$213.4D4$242.3D3.2D11.D24.D3.3D3.3D3.3D3.3D$211.C11.D17.D3.D3.D4.D
6.D23.D.D.D3.D.D3.D.D3.D.D$210.2C10.D2.5D.5D5.D7.D8.4D2.3D2.D.2D6.4D
3.D6.D.D2.2D5.D.D$210.C.C8.D19.D2.2D3.D4.D2.D3.D.D3.D.2D2.D5.D3.D8.D
2.D.D.D4.D2.4D$222.D2.5D.5D5.D3.D3.D4.D2.D3.D.5D.D9.D3.D7.D3.2D2.D3.D
3.D3.D$223.D17.D3.D3.D4.D2.D3.D.D5.D9.D3.D6.D4.D3.D2.D4.D3.D$242.3D4.
2D3.2D2.4D2.4D.D9.D3.D5.5D2.3D2.5D2.3D$204.4D$204.2D$204.D.D$204.D2.D
$208.D$209.D$210.D$161.3D4.D24.D17.D$160.D6.2D9.D5.D8.D18.D$160.D7.D
7.5D7.4D.D2.D3.4D9.D$160.4D4.D9.D5.D2.D5.D.D3.D14.D10.3E$160.D3.D3.D
9.D5.D2.D5.3D4.3D12.D2.D6.E$160.D3.D3.D9.D5.D2.D5.D2.D6.D12.D.D7.E$
161.3D3.3D9.2D3.2D2.4D.D3.D.4D14.2D$215.4D18$249.E$248.2E$248.E.E!
Once Glider n°2027 is advanced, the Glider n°2026 (+following Gliders) can be advanced without changing the final reaction :

Code: Select all

x = 303, y = 219, rule = LifeHistory
19.2A$20.A$18.A$18.5A15.A$23.A13.A.A$20.3A14.A.A$19.A18.A$19.4A$17.2A
3.A3.2A5.2A$16.A2.3A4.2A4.A2.A11.3A$16.2A.A13.2A$19.A25.A$19.2A24.A$
45.A$49.2A$48.A.A$48.2A34$74.A$74.A6.2A$74.A6.2A2.2A$85.2A4$93.3A$75.
2A2.2A$75.2A2.2A2$71.A$70.A.A7.2A$2A69.A7.3A$2A$79.A$73.3A3.2A$79.A7$
98.A$97.2A$97.A.A36$136.A$135.2A$135.A.A34$170.3A$170.A$171.A9$193.4D
11.3D4.D24.D$193.2D12.D6.2D9.D5.D8.D$193.D.D11.D7.D7.5D7.4D.D2.D3.4D$
193.D2.D10.4D4.D9.D5.D2.D5.D.D3.D$186.2A9.D9.D3.D3.D9.D5.D2.D5.3D4.3D
$185.2A11.D8.D3.D3.D9.D5.D2.D5.D2.D6.D$187.A11.D8.3D3.3D9.2D3.2D2.4D.
D3.D.4D$200.D$201.D$202.D$203.D$204.D2.D$205.D.D$206.2D$204.4D4$233.
3D3.2D11.D24.D3.3D3.3D3.3D3.3D$201.2C11.D17.D3.D3.D4.D6.D23.D.D.D3.D.
D3.D.D3.D.D$201.C.C9.D2.5D.5D5.D7.D8.4D2.3D2.D.2D6.4D3.D6.D.D2.2D5.D.
D$201.C10.D19.D2.2D3.D4.D2.D3.D.D3.D.2D2.D5.D3.D8.D2.D.D.D4.D2.4D$
213.D2.5D.5D5.D3.D3.D4.D2.D3.D.5D.D9.D3.D7.D3.2D2.D3.D3.D3.D$214.D17.
D3.D3.D4.D2.D3.D.D5.D9.D3.D6.D4.D3.D2.D4.D3.D$233.3D4.2D3.2D2.4D2.4D.
D9.D3.D5.5D2.3D2.5D2.3D$195.4D$195.2D$195.D.D$195.D2.D$199.D$200.D$
201.D$152.3D4.D24.D17.D$151.D6.2D9.D5.D8.D18.D$151.D7.D7.5D7.4D.D2.D
3.4D9.D12.E$151.4D4.D9.D5.D2.D5.D.D3.D14.D10.2E$151.D3.D3.D9.D5.D2.D
5.3D4.3D12.D2.D6.E.E$151.D3.D3.D9.D5.D2.D5.D2.D6.D12.D.D$152.3D3.3D9.
2D3.2D2.4D.D3.D.4D14.2D$206.4D18$239.2E$239.E.E$239.E!
So a second pass (or more) is needed to ensure to reach the maximum compression.
In fact, the second pass finds a new right timing between some Gliders that change the reaction but not the result.
Alone we go faster … Together we go further …
Avatar's pattern
My Sandbox
Bom-Bom
ℝ - ℕ = ℝ or ℕ ⊄ ℝ

User avatar
rabbit
Posts: 193
Joined: March 4th, 2024, 6:00 am
Location: Stuck inside a magician's hat. it hurts so bad

Re: Golly scripts

Post by rabbit » July 31st, 2025, 3:43 pm

Here's a lua script I created and used a couple times for rulegolfing. It can be used find variants of a given rule where a movement engine lasts the longest while remaining unstable.

Code: Select all

--[[
	lms.lua, rabbit, 2025-07-<15?
	(The name is short for "local maximum search", but I don't know if I used that term properly)
	This script explores every one-transition variant of a rule, assigning each a score based on
		the presence of a cell shifting over time and choosing the highest score, repeating until
		no variant has a higher score.
	For example, with origin (0, 0), offset (1, 0) and step 2, a b-heptomino with its leading cell
		at the origin scores 7, because it takes 7 steps for that cell to not reappear.
	It also contains parameters for delaying the creation of the frontend (start_gen),
		disqualifying patterns that last too long (max_gen), and disqualifying patterns that reach
		a high population (max_pop).
	There is also a transition blacklist (cfgs_no) for transitions you want to keep/avoid.
	This utility can be used to aid rulegolfing.
	It also contains my implementation of a rulestring->transitions and transitions->rulestring
		converter. Feel free to use those!
		
	 () ()
	(='Y'=)
]]

local g = golly()

--change these

local start_x, start_y = 0, 0
local offset_x, offset_y = 1, 0
local start_gen = 0
local step_gen = 2
local max_gen = 1000
local max_pop = math.huge --200

local cfgs_no = {B0 = true, B1c = true, B1e = true, B2a = true, B2c = true, B2e = true, S0 = true, S1c = true, S1e = true} --, B4a = true, S4a = true, S4i = true, S5i = true, S5n = true} --this is a dict

--don't change anything below this
local configs = {[0] = {""}, 
	[1] = {"c", "e"}, 
	[2] = {"a", "c", "e", "i", "k", "n"}, 
	[3] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "y"}, 
	[4] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "t", "w", "y", "z"}, 
	[5] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "y"}, 
	[6] = {"a", "c", "e", "i", "k", "n"}, 
	[7] = {"c", "e"}, 
	[8] = {""}
}

function break_rule(s) --turns a rule into a table of transitions, so that they can be added to or removed from freely
	local broken = {} --dict with birth conditions, survival conditions
	local bors = "B"
	local num, negative
	local has_letters = true --for first loop only

	function add_all(num)
		for _, ltr in ipairs(configs[num]) do
			broken[bors..num..ltr] = true
		end
	end

	for i = 2, string.len(s)+1 do
		local chr = string.sub(s, i, i)
		
		local c2num = tonumber(chr)
		if c2num then
			--if the numbers are adjacent go back and add transitions
			if not has_letters then
				add_all(num)
			end

			num = c2num
			negative, has_letters = false, false
		elseif chr == "-" then
			add_all(num) --subtract later
			negative = true
		elseif chr == "/" or chr == "" then
			--kind of a hack to fix cases where a section ends with a number. this is why the loop runs for +1 iterations btw
			if not has_letters then
				add_all(num)
			end
			has_letters = true --stops it from adding to survival as well
		elseif chr == "S" then
			bors = "S"
		else --if chr is a letter
			local trans = bors..num..chr
			if not negative then
				broken[trans] = true
			else
				broken[trans] = nil
			end
			has_letters = true
		end
	end

	return broken
end

function make_rule(b) --converts a table of transitions into a rulestring
	local str = ""
	local decomp = {B0 = {}, B1 = {}, B2 = {}, B3 = {}, B4 = {}, B5 = {}, B6 = {}, B7 = {}, B8 = {}, S0 = {}, S1 = {}, S2 = {}, S3 = {}, S4 = {}, S5 = {}, S6 = {}, S7 = {}, S8 = {}}

	for trans, _ in pairs(b) do
		local group, chr = string.sub(trans, 1, 2), string.sub(trans, 3, 3)
		table.insert(decomp[group], chr)
	end

	for _, bors in pairs({"B", "S"}) do
		str = str .. bors

		for num = 0, 8 do
			local current, total = decomp[bors .. num], configs[num]
			if #current ~= 0 then
				str = str .. num
				
				if #current == #total then
					--this line intentionally left blank
				else
					if #current <= math.ceil(#total / 2) then
						table.sort(current)
						for _, chr in pairs(current) do
							str = str .. chr
						end
					else
						str = str .. "-"

						--create a dict version of current
						local cur_dict = {}
						for _, chr in pairs(current) do
							cur_dict[chr] = true
						end

						--now compare against total
						for _, chr in ipairs(total) do
							if cur_dict[chr] == nil then
								str = str .. chr
							end
						end
					end
				end
			end
		end

		if bors == "B" then
			str = str .. "/"
		end
	end

	return str
end

function copy_dict(t)
	local n = {}
	for k, v in pairs(t) do
		n[k] = v
	end
	return n
end

function evaluate(str) --evaluate current pattern for rulestring str
	g.reset()
	g.setrule(str)
	g.run(start_gen)
	for i = 0, max_gen / step_gen - 1 do
		local x, y = start_x + offset_x * i, start_y + offset_y * i
		if g.getcell(x, y) == 0 then
			if tonumber(g.getpop()) > max_pop then
				return -1
			end

			return i - 1
		end
		g.run(step_gen)
	end
	return -1
end

function find_best_variant(str) --try each single-transition variant of the rulestring str and return that with the highest score
	local broken = break_rule(str)
	local best_rule, best_score = "", -2

	for _, bors in pairs({"B", "S"}) do
		for num, list in pairs(configs) do
			for _, chr in pairs(list) do
				local trans = bors..num..chr
				if not cfgs_no[trans] then
					local new_broken = copy_dict(broken)
					if not new_broken[trans] then
						new_broken[trans] = true
					else
						new_broken[trans] = nil
					end
					local new_str = make_rule(new_broken)
					local score = evaluate(new_str)
					if score > best_score then --idk how to deal with tiebreakers
						best_rule, best_score = new_str, score
					end
				end
			end
		end
	end

	return best_rule, best_score
end

local c = g.getcells(g.getrect())
g.new("")
g.putcells(c)

local rule_list = {}
local toppest_rule, toppest_score = "", -2
while true do
	local top_rule, top_score = find_best_variant(g.getrule())
	g.setrule(top_rule)
	if top_score > toppest_score then
		toppest_rule, toppest_score = top_rule, top_score
		table.insert(rule_list, {top_rule, top_score})
	else
		g.reset()
		g.setrule(toppest_rule)

		local estr = toppest_rule .. " has a score of " .. toppest_score .. " | ("
		for _, result in pairs(rule_list) do
			local rule, score = result[1], result[2]
			estr = estr .. rule .. ": " .. score ..", "
		end
		local l = string.len(estr)
		estr = string.sub(estr, 1, l-2) .. ")"

		g.exit(estr)
		break --?
	end
end

--[[local broken = break_rule(g.getrule())
local made = make_rule(broken)

g.warn(made)]]
That's the bunny. (she/her)

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » November 14th, 2025, 6:14 am

Here's a program that I made for rulegolfing. It uses a genetic-based algorithm that scores rules by closeness to a certain density or change in density throughout a certain number of steps.
(Default is speedydelete's ruleset)

Code: Select all

import random, math, re
from collections import deque
import golly as g

minrule = "B2cen5j/S12-ik" # Minimum for the rule range
maxrule = "B2-ak3cy4-jqry5-c678/S12-k3-knqry45678" # Maximum for the rule range
chance = 30 # Rule generation chance (Can change the variety of rules)
flip_chance = 3 # Rule flipping chance (restricted to maximum rule)
entropy_chance = 0.5 # Rule flipping chance (unrestricted)
entropy_max = maxrule # Maximum for entropy (Mainly to remove garbage rules)
desired_density = 0.05 # Density (pop(t) / bbox)
desired_activity = 0.007 # Activity ((pop(t) - pop(t - 1)) / bbox)
threshold = 20 # Threshold for matching (using abs(r-v)*t<1 where v is the desired value, r is the real value, and t is the threshold)
first_threshold = 4 # Threshold, but before any rules are found
pre_steps = 20 # Steps to run before collecting data
num_steps = 80 # Steps to run while collecting data
num_rules = 5 # Number of rules to generate
max_rules = 50 # Maximum stored rules

hensel1 = {
    0: "",
    1: "ce",
    2: "acekin",
    3: "acekijnqry",
    4: "acekijnqrtwyz",
    5: "acekijnqry",
    6: "acekin",
    7: "ce",
    8: ""
}

hensel2 = ['0', '1c', '1e', '2a', '2c', '2e', '2k', '2i', '2n', '3a', '3c', '3e', '3k', '3i', '3j', '3n', '3q', '3r', '3y', '4a', '4c', '4e', '4k', '4i', '4j', '4n', '4q', '4r', '4t', '4w', '4y', '4z', '5a', '5c', '5e', '5k', '5i', '5j', '5n', '5q', '5r', '5y', '6a', '6c', '6e', '6k', '6i', '6n', '7c', '7e', '8']

def fix_hensel(num):
    if len(num) == 1: return num + hensel1[int(num)]
    if len(num.split("-")) == 1: return num
    o, h = num.split("-")
    h = "".join(sorted(h))
    n = ""
    for s in hensel1[int(o)]:
        if s not in h:
            n += s
    return o + n

def expand_hensel(rule):
    rb = rule[1:].split("/S")[0]
    rs = rule.split("/S")[1]
    rb1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rb))]
    rs1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rs))]
    rb2 = []
    for r in rb1:
        if len(r) == 1:
            rb2.append(r)
        for rr in r[1:]:
            rb2.append(r[0] + rr)
    rs2 = []
    for r in rs1:
        if len(r) == 1:
            rs2.append(r)
        for rr in r[1:]:
            rs2.append(r[0] + rr)
    fin = []
    for h in hensel2:
        if h not in rb2:
            fin.append(0)
        else:
            fin.append(1)
    for h in hensel2:
        if h not in rs2:
            fin.append(0)
        else:
            fin.append(1)
    return fin

nr = expand_hensel(minrule)
xr = expand_hensel(maxrule)

rr = list(zip(nr, xr))
er = expand_hensel(entropy_max)

def generate_rule(rr, chance):
    o = g.getrule()
    r = f"B{''.join(hensel2[i] for i, t in enumerate(rr[:51]) if t[0] or (random.random() * 100 < chance and t[1]))}/S{''.join(hensel2[i] for i, t in enumerate(rr[51:]) if t[0] or (random.random() * 100 < chance and t[1]))}"
    g.setrule(r)
    r2 = g.getrule()
    g.setrule(o)
    return r2
    
def generate_rule_randflip(rr, chance):
    o = g.getrule()
    r = f"B{''.join(hensel2[i] for i, t in enumerate(rr[:51]) if (t[0] if random.random() * 100 < chance else t[1]) ^ ((random.random() * 100 < flip_chance and not t[0]) and t[1] or random.random() * 100 < entropy_chance and er[i]) or t[0])}/S{''.join(hensel2[i] for i, t in enumerate(rr[51:]) if (t[0] if random.random() * 100 < chance else t[1]) ^ ((random.random() * 100 < flip_chance and not t[0]) and t[1] or random.random() * 100 < entropy_chance and er[i]) or t[0])}"
    g.setrule(r)
    r2 = g.getrule()
    g.setrule(o)
    return r2
    
def generate_rules(rr, chance):
    result = []
    for i in range(num_rules):
        result.append(generate_rule_randflip(rr, chance))
    return result

running = True
rect = g.getselrect()

first = 0
def is_bad(gr, dr):
    t = (threshold - first_threshold) * (1 - 1 / (1 + first)) + first_threshold
    if abs(gr - desired_activity) * t > desired_activity: return 1
    if abs(dr - desired_density) * t > desired_density: return 1
    return 0

rrs = [rr]
total_soup_count = 0
soup_update = 5

def score_rule(rr): 
    global total_soup_count
    results = []
    agr, adr = 0, 0
    i = 0
    for s in generate_rules(rr, 100 - chance):
        i += 1
        g.clear(0)
        g.clear(1)
        g.setrule(s)
        g.randfill(50)
        gr, dr = 0, 0
        for n in range(pre_steps + num_steps):
            p1 = int(g.getpop())
            r1 = g.getrect()
            if not (r1 and p1): break
            g.step()
            p2 = int(g.getpop())
            r2 = g.getrect()
            if not (r2 and p2): break
            if n > pre_steps:
                gr += ((p2 - p1) / (r2[2] * r2[3] * num_steps)) ** 2
                dr += (p2 / (r2[2] * r2[3] * num_steps)) ** 2
        total_soup_count += 1
        gr = math.sqrt(gr * num_steps)
        dr = math.sqrt(dr * num_steps)
        agr += gr
        adr += dr
        if is_bad(gr, dr): continue
        nr = expand_hensel(s)
        xr = expand_hensel(s)
        rr = list(zip(nr, xr))
        results.append(rr)
    return results, agr / i, adr / i

def score_closeness(r):
    s = 0
    for i in range(102):
        if r[i][0] and r[i][0] and r[i][1] and r[i][1]:
            s += random.random()
    return s

i = 0
def must_quit(e):
    return e.startswith("key q")
def must_copy(e):
    return e.startswith("key c")

def format_rr(rr):
    n = generate_rule(rr, 0)
    x = generate_rule(rr, 100)
    if n != x:
        return f"{n} - {x}"
    else:
        return n

while running:
    g.new("Sylvani's Rule Search")
    rrs2 = deque([])
    g.select(rect)
    g.setpos(str(rect[0]+rect[2]//2), str(rect[1]+rect[3]//2))
    all_rules = list(map(format_rr, rrs))
    all_rules = list(set(all_rules))
    rl = len(rrs)
    pgr, pdr = [], []
    for rr in rrs:
        e = g.getevent()
        if must_quit(e):
            running = False
            break
        if must_copy(e):
            g.setclipstr('\n'.join(all_rules))
        res, agr, adr = score_rule(rr)
        first = (first + len(res) / (1 + rl)) / (1 + first)
        t = (threshold - first_threshold) * (1 - 1 / (1 + first)) + first_threshold
        if adr * t < 1 and agr * t < 1:
            rrs2.extend(res)
        i += num_rules
        g.select([])
        g.update()
        g.select(rect)
        g.show(f"{len(rrs)} rules found after {i} attempts. Average scores: D={adr:.4}, A={agr:.4}; Press q to quit, or c to copy the results to the clipboard. ({', '.join(all_rules)})")
    rrs.extend(rrs2)
    rrs = sorted(rrs, key=score_closeness, reverse=True)
    rrs = list(map(list, list(set(map(tuple, rrs)))))
    rrs = rrs[:max_rules]
    random.shuffle(rrs)
g.setclipstr('\n'.join(all_rules))

User avatar
Sylvani
Posts: 146
Joined: September 26th, 2024, 3:23 am

Re: Golly scripts

Post by Sylvani » February 1st, 2026, 4:00 pm

Here's a much better version that uses gzip compression as a parameter (trust me, it can work well occasionally). Be advised that the screen might flash as soups are generated.

Code: Select all

import random, math, re, gzip, struct
import golly as g
minrule = "B3aijn/S2ae3jnr" # Minimum for the rule range
maxrule = "B34-r5-n678/S234-k5678" # Maximum for the rule range
chance = 30 # Rule generation chance (Can change the variety of rules)
fill_density = 50 # Random fill density
flip_chance = 2 # Rule flipping chance (restricted to maximum rule)
pre_steps = 50 # Steps to run before testing
test_steps = 200 # Steps to run while testing
num_rules = 100 # Number of rules
num_children = 10 # Number of children the rules produce
density = 0.1 # Density of the cells
growth_rate = 0.02 # Growth rate of the population
compression = 0.3 # Gzip compression rate
threshold = 0.05 # Maximum deviation from the wanted values


hensel1 = {
    0: "",
    1: "ce",
    2: "acekin",
    3: "acekijnqry",
    4: "acekijnqrtwyz",
    5: "acekijnqry",
    6: "acekin",
    7: "ce",
    8: ""
}

hensel2 = ['0', '1c', '1e', '2a', '2c', '2e', '2k', '2i', '2n', '3a', '3c', '3e', '3k', '3i', '3j', '3n', '3q', '3r', '3y', '4a', '4c', '4e', '4k', '4i', '4j', '4n', '4q', '4r', '4t', '4w', '4y', '4z', '5a', '5c', '5e', '5k', '5i', '5j', '5n', '5q', '5r', '5y', '6a', '6c', '6e', '6k', '6i', '6n', '7c', '7e', '8']

def fix_hensel(num): 
    if len(num) == 1: return num + hensel1[int(num)]
    if len(num.split("-")) == 1: return num
    o, h = num.split("-")
    h = "".join(sorted(h))
    n = ""
    for s in hensel1[int(o)]:
        if s not in h:
            n += s
    return o + n

def expand_hensel(rule):
    rb = rule[1:].split("/S")[0]
    rs = rule.split("/S")[1]
    rb1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rb))]
    rs1 = [fix_hensel(r) for r in filter(lambda x: x, re.split(r"(\d-?[acekijnqrtwyz]*)", rs))]
    rb2 = []
    for r in rb1:
        if len(r) == 1:
            rb2.append(r)
        for rr in r[1:]:
            rb2.append(r[0] + rr)
    rs2 = []
    for r in rs1:
        if len(r) == 1:
            rs2.append(r)
        for rr in r[1:]:
            rs2.append(r[0] + rr)
    fin = []
    for h in hensel2:
        if h not in rb2:
            fin.append(0)
        else:
            fin.append(1)
    for h in hensel2:
        if h not in rs2:
            fin.append(0)
        else:
            fin.append(1)
    return fin

nr = expand_hensel(minrule)
xr = expand_hensel(maxrule)

def gen_rule(mn, mx, chance, flip=[0]*102):
    btrans = ""
    strans = ""
    bits = []
    for i in range(51):
        if mx[i]:
            if ((random.random() * 100 < chance) ^ flip[i]) or mn[i]:
                btrans += hensel2[i]
                bits.append(1)
            else:
                bits.append(0)
        else:
            bits.append(0)
    for i in range(51, 102):
        if mx[i]:
            if ((random.random() * 100 < chance) ^ flip[i]) or mn[i]:
                strans += hensel2[i - 51]
                bits.append(1)
            else:
                bits.append(0)
        else:
            bits.append(0)
    old_rule = g.getrule()
    g.setrule(f"B{btrans}/S{strans}")
    new_rule = g.getrule()
    return bits, new_rule, old_rule

rules = [(gen_rule(nr, xr, chance)[0], 1)]
temperature = 1

def test_rule(rule):
    bits, new_rule, old_rule = gen_rule(nr, xr, flip_chance, rule)
    g.clear(0)
    g.clear(1)
    g.randfill(fill_density)
    for i in range(pre_steps):
        g.run(1)
    dn = 0
    gr = 0
    cr = 0
    selrect = g.getselrect()
    for i in range(test_steps):
        pop = len(cells := g.getcells(selrect)) // 2
        pop1 = int(g.getpop())
        if not pop1:
            break
        g.run(1)
        pop2 = int(g.getpop())
        dn += pop / (selrect[2] * selrect[3]) / test_steps
        gr += (pop2 / pop1) / test_steps
        buf = struct.pack('%si' % len(cells), *cells)
        if not buf:
            continue
        cbuf = gzip.compress(buf)
        cr += (len(cbuf) / len(buf)) / test_steps
    return dn, (gr - 1), cr, bits



running = True
oldsel = g.getselrect()
g.new("Rule search")
g.select(oldsel)
g.fitsel()
best = 999
best_rules = []
while running:
    if g.getevent().startswith("key q"):
        running = False
    if len(rules) == 0:
        rules.append((gen_rule(nr, xr, chance)[0], temperature))
    rules = sorted(rules, key=lambda k: k[1], reverse=True)
    rules2 = [*filter(lambda r: r[1] <= best, rules)]
    if rules[0][1] < best:
        best = rules[0][1]
        best_rules.append(rules[0])
        best_rules = sorted(best_rules, key=lambda k: k[1])
    if len(rules2) == 0:
        rules2.extend(best_rules)
    rules = []
    for rule, tmp in rules2:
        if g.getevent().startswith("key q"):
            running = False
            break
        dn, gr, cr, bits = test_rule(rule)
        g.update()
        g.show(str((dn, gr, cr, tmp, best)))
        if (dn > density - threshold) and (dn < density + threshold) and (gr > growth_rate - threshold) and (gr < growth_rate + threshold) and (cr > compression - threshold) and (cr < compression + threshold):
            tmp /= 1.01
            if tmp < best:
                rules.extend([(gen_rule(nr, xr, flip_chance, flip=bits)[0], tmp) for _ in range(num_children)])
            rules.append((bits, tmp))
            rules = sorted(rules, key=lambda k: k[1], reverse=True)
        while len(rules) > num_rules:
            rules.pop()
g.setclipstr("\n".join(gen_rule(r, r, 0)[1] for r, t in best_rules))
gen_rule(best_rules[0][0], best_rules[0][0], 0)

User avatar
qqd
Posts: 605
Joined: September 10th, 2022, 4:24 pm
Location: In a superposition of multiple different locations.

Re: Golly scripts

Post by qqd » March 7th, 2026, 12:26 pm

Here is a Lua script for converting rule files (files ending with .rule, meant for use with the RuleLoader algorithm) using the oneDimensional neighborhood and a @TABLE structure into the equivalent 2D spacetime diagram of the rule:

Code: Select all

-- spacetime1Druleconverter.lua
-- By qqd (conwaylife.com forum user)
-- This is a script to convert a .rule file using the oneDimensional neighborhood into 
-- the equivalent 2D rule which does a spacetime projection of the 1D rule
-- This helps with rules like W110 which look are hard to understand on a 1D scale but reveal their patterns depicted as a 2D spacetime diagram
-- This also allows you to run spacetime diagrams of your 1D rules at Golly speeds 
-- because it does not require the overlay overhead of 1D.lua
-- I realize that this may be a bit advanced for a first script and may be a little inconsistent, but then again it really does not matter
-- Note: all variables containing files, file names or paths are in all caps and have underscores
-- Please do note that this script does not check the rule file's validity (apart from whether it is a rule file by
-- checking if the first line has @RULE) so giving it an invalid file may cause it throw errors or give a corrupted rule file
-- Known bugs (only minor so far):
--[[
Fails horribly if there is leading whitespace before any lines beginning with "@(rulecomponent)" or "var" or "#" (but trailing whitespace is discouraged anyway)
Does not preserve blank lines after "@TABLE" (minor bug)
--]]


local g = golly()
-- require "gplus.strict"
-- This 2 line combo is used quite a lot, so it is packed in a function for conciseness
function g.warnandexit(warning)
    g.warn(warning)
    g.exit()
end

local RULE_TO_CONV = g.getstring("Enter the name of the rule saved in your rules folder (do not specify .rule)", 
                                g.getrule(),
                                "Enter 1D rule to convert")
g.show("Opening rule file...")
local FILE_PATH = g.getdir("rules")
local RULE_FILE, err  = io.open(FILE_PATH..RULE_TO_CONV..".rule", "r") -- Open the rule file in read mode
if RULE_FILE then
    local ruletabletxt = {}
    g.show("Parsing rule file...")
    for line in RULE_FILE:lines() do
        if line then
            ruletabletxt[#ruletabletxt + 1] = (line:gsub("[\r\n]", "")) -- Wrapping a function in parantheses makes it return only the first value
        end
    end
    g.show("Closing rule file..")
    RULE_FILE:close()
    g.show("Parsing rule text...")
    if not ruletabletxt[1] == "@RULE ".. RULE_TO_CONV then
        g.warnandexit("Rule file is invalid!")
    end
    local description = ""
    local tablestart = false
    for index, strline in ipairs(ruletabletxt) do
        if strline == "@TABLE" then
           tablestart = index
           break
        end
        if index > 1 then -- Ignore the initial @RULE line
            description  = description..strline.."\n"
        end
    end
    for index = #ruletabletxt, 1, -1 do -- Going in reverse to ensure order stays the same while removing items
        if index < tablestart then
            table.remove(ruletabletxt, index)
        elseif ruletabletxt[index] == "" then
            table.remove(ruletabletxt, index)
        end
    end
    if not tablestart then
        g.warnandexit("Sorry! We do not support 1D rules using only @TREE yet.")
    end
    tablestart = nil -- Remove temporary variable
    local n_states = ruletabletxt[2]:match("^n_states:%s*(%d+)%s*$")
    if not ruletabletxt[3]:find("oneDimensional") then
        g.warnandexit("The rule specified does not use the oneDimensional neighborhood!")
    end
    local isreflectsymmetry = not ruletabletxt[4]:find("none") 
    local initrulestr = "@RULE "..RULE_TO_CONV.."-spacetime\n\n(Created using spacetime1Druleconverter.lua using '"..RULE_TO_CONV..".rule')\n"
                        ..description..
                        "@TABLE\n"
                        ..ruletabletxt[2]..
                        "\nneighborhood:Moore\nsymmetries:none\n\n"
    local newruletable = {} -- New rule table (excluding initial @RULE (rule name) + comments before @TABLE)
    local varnames = {} -- Stores the variable names originally defined in the .rule file
    local _anyne = "_anyne"
    local _anynw = "_anynw"
    local endedtable = false -- Checks if the transition table has ended
    for index = 5, #ruletabletxt do
        g.empty() -- No-op g (golly) function so that Golly checks for the escape key for aborting a script
        local line = ruletabletxt[index]
        local linelen = #line
        if endedtable then
            if line:byte(1) == string.byte("@") then
                newruletable[#newruletable + 1] = "\n"..line
            else
                newruletable[#newruletable + 1] = line
            end
        elseif line:byte(1) == string.byte("@") then
            -- This is the start of @TREE, @NAMES, @COLORS, or @ICONS so add and set endedtable to true
            -- But before we do that, we need to add some catch-all transitions
            states = {}
            for state = 1, n_states do
                states[state] = state - 1
            end
            newruletable[#newruletable + 1] = "\n# Catch-all variables and transitions (for the spacetime version)\n"
            newruletable[#newruletable + 1] = "var ".._anyne.." = {"..table.concat(states, ",").."}"
            newruletable[#newruletable + 1] = "var ".._anynw.." = ".._anyne
            states = nil
            for catchall = 0, n_states - 1 do
                newruletable[#newruletable + 1] = "0,"..catchall..",".._anyne..",0,0,0,0,0,".._anynw..","..catchall
            end
            -- The catch-all transitions have now been added
            newruletable[#newruletable + 1] = "\n"..line
            endedtable = true
        elseif line:byte(1) == string.byte("#") then
            -- This is a comment line so directly add it to newruletable
            newruletable[#newruletable + 1] = line
        elseif line:sub(1, 4) == "var " then
            -- This line defines a variable, so some extra logic is required
            newruletable[#newruletable + 1] = line
            -- Checks for variable collisions
            countof_ = #(line:match("^var%s+([^%s=]+)"):match("^(_+)anyn[ew]$") or "") 
            -- The above is the count of underscores in the variable name defined
            -- (The first match extracts the variable)
            if countof_ + 6 > #_anyne then
                _anyne = string.rep("_", countof_).._anyne
            end
            if countof_ + 6 > #_anynw then
                _anynw = string.rep("_", countof_).._anynw
            end  
        else
            -- This is an actual transition line (unless you have incorrect syntax or trailing whitespace)
            local istate, wstate, estate, ostate, comment = line:match("^(.-,)%s*(.-,)%s*(.-,)%s*(.-)(%s*#?.-)%s*$")
            -- do note that all variables except 'ostate' and 'comment' have a comma (and maybe whitespace) attached to them
            -- this reduces the complexity of the pattern to match as well as reducing the complexity of the concatenation of the new transition
            -- g.note(tostring(index).."(index).. "..table.concat(transition, ", "))
            if not istate then
                -- This is for supporting traditional rules with less than 10 states or less and no variables
                -- that allow "0,1,2,7,8" to be written as "01278"
                local istate = line:sub(1,1)..","
                local wstate = line:sub(2,2)..","
                local estate = line:sub(3,3)..","
                local ostate = line:sub(4,4)..","
                local comment = line:match("(%s*#?.-)%s*$")
            end
            newruletable[#newruletable + 1] = "0,"..istate..estate.."0,0,0,0,0,"..wstate..ostate..comment
            if isreflectsymmetry then
                newruletable[#newruletable + 1] = "0,"..istate..wstate.."0,0,0,0,0,"..estate..ostate
            end
        end        
    end
    -- Add catch all transitions if the @TABLE section has not yet ended (i.e. there are no other headers in the file)
    if not endedtable then
        states = {}
        for state = 1, n_states do
            states[state] = state - 1
        end
        newruletable[#newruletable + 1] = "\n# Catch-all variables and transitions (for the spacetime version)\n"
        newruletable[#newruletable + 1] = "var ".._anyne.." = {"..table.concat(states, ",").."}"
        newruletable[#newruletable + 1] = "var ".._anynw.." = ".._anyne
        states = nil
        for catchall = 0, n_states - 1 do
            newruletable[#newruletable + 1] = "0,"..catchall..",".._anyne..",0,0,0,0,0,".._anynw..","..catchall
        end
    end
    g.show("Opening new rule file...")
    local NEW_RULE_FILE <close> = assert(io.open(FILE_PATH.."/"..RULE_TO_CONV.."-spacetime.rule", "w"))
    -- The above line safely opens a new .rule file so that it will automatically close even if the script crashes from this point onwards
    g.show("Writing new rule text")
    NEW_RULE_FILE:write(initrulestr..table.concat(newruletable, "\n"))
    g.show("Closing new rule file...")
    NEW_RULE_FILE:close()
    g.show("Spacetime rule file successfully created with the rule name '"..RULE_TO_CONV.."-spacetime'!")
    local answer = g.query("Rule Created!", "Spacetime rule file successfully created with the rule name '"..RULE_TO_CONV.."-spacetime'!\nWould you like to swtich to this rule?")
    if answer == "Yes" then
        g.setrule(RULE_TO_CONV.."-spacetime")
        g.show("Rule switched to "..RULE_TO_CONV.."-spacetime.")
    end
else
    g.show("No rule file found!")
    g.warn("Rule file does not exist! ("..err..")")
end
As stated in the script, it is very useful for revealing hidden patterns in otherwise opaque 1D CA. For example, defining Wolfram110 as a rule file like this: (EDIT by Andrew: changed OneDimensional to oneDimensional)

Code: Select all

@RULE Wolfram110

This is a rule file for Wolfram 110.
It is an elementary cellular automaton proven Turing-Complete by Matthew Cook in 2004.

@TABLE
n_states:2
neighborhood:oneDimensional
symmetries:none
0,1,0,1
0,1,1,1
1,1,1,0 # Just 3 transition lines are enough, as in all other cases the center cell retains its state

@COLORS
0 0 0 0
1 255 255 255
...makes it very difficult to notice emergent behavior, as all of the activity happens on a single 1 dimensional line of cells.
However, using the above Lua script (assuming you have saved the above rule as 'Wolfram110.rule' in your rules folder, whose path is specified in Golly > File > Preferences > Control) allows you to convert the rule into a new rule file that simulates the spacetime version of the original rule. The output file should look like this:

Code: Select all

@RULE Wolfram110-spacetime

(Created using spacetime1Druleconverter.lua using 'Wolfram110.rule')

This is a rule file for Wolfram 110.
It is an elementary cellular automaton proven Turing-Complete by Matthew Cook in 2004.

@TABLE
n_states:2
neighborhood:Moore
symmetries:none

0,0,1,0,0,0,0,0,0,1
0,0,1,0,0,0,0,0,1,1
0,1,1,0,0,0,0,0,1,0 # Just 3 transition lines are enough, as in all other cases the center cell retains its state

# Catch-all variables and transitions (for the spacetime version)

var _anyne = {0,1}
var _anynw = _anyne
0,0,_anyne,0,0,0,0,0,_anynw,0
0,1,_anyne,0,0,0,0,0,_anynw,1

@COLORS
0 0 0 0
1 255 255 255
In this new rule, it is very easy to see Rule 110's emergent complexity and structure, as compared to the original rule file.
This Lua script should (ignoring any unnoticed bugs) work with any rule file using the oneDimensional neighborhood and the @TABLE structure, including ones with:
  • Custom variables
  • Comments (including in-line comments)
  • All symmetries of the oneDimensional neighborhood listed on the RoadMap as of this writing.
  • Transition lines with 'traditional' syntax (in rules with less than 10 states and no variables, transitions can be written as '01278' instead of '0,1,2,7,8')
Last edited by qqd on May 1st, 2026, 1:13 am, edited 2 times in total.
Currently writing a utility in Lua that may be helpful for faster manual pattern manipulation.

User avatar
rabbit
Posts: 193
Joined: March 4th, 2024, 6:00 am
Location: Stuck inside a magician's hat. it hurts so bad

Re: Golly scripts

Post by rabbit » March 18th, 2026, 4:31 am

Barring undetected bugs left undiscovered in my initial testing, this lua script gives the range of rules that a pattern works in, up to an amount of generations specified by the user:

Code: Select all

--[[
	ranger.lua, rabbit, 2026-03-18
	Finds the minimum and maximum INT rule that a given pattern works in.
]]

local g = golly()

local letters = ".ceaccaieaeaknja.ceaccaieaeaknjaekejanaairerririekejanaairerririccknncqnaijaqnwaccknncqnaijaqnwakykkqyqjrtjnzrqakykkqyqjrtjnzrqaekirkyrtejerkkjnekirkyrtejerkkjnekejjkrnejecjyccekejjkrnejecjyccanriqyzraariqjqaanriqyzraariqjqajkjywkqkrnccqkncjkjywkqkrnccqknccnkqccnnkqkqyykjcnkqccnnkqkqyykjaqjwinaarzjqtrnaaqjwinaarzjqtrnaccyyccyennkjyekeccyyccyennkjyekenykknejeirykrikenykknejeirykrikeaqrznyirjwjqkkykaqrznyirjwjqkkykaqrqajiarqcnnkccaqrqajiarqcnnkccintrneriaanajekeintrneriaanajekeajnkaeaeiaccaec.ajnkaeaeiaccaec."

----the following is borrowed from my lms.lua script

local configs = {[0] = {""}, 
	[1] = {"c", "e"}, 
	[2] = {"a", "c", "e", "i", "k", "n"}, 
	[3] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "y"}, 
	[4] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "t", "w", "y", "z"}, 
	[5] = {"a", "c", "e", "i", "j", "k", "n", "q", "r", "y"}, 
	[6] = {"a", "c", "e", "i", "k", "n"}, 
	[7] = {"c", "e"}, 
	[8] = {""}
}

function break_rule(s) --turns a rule into a table of transitions, so that they can be added to or removed from freely
	local broken = {} --dict with birth conditions, survival conditions
	local bors = "B"
	local num, negative
	local has_letters = true --for first loop only

	function add_all(num)
		for _, ltr in ipairs(configs[num]) do
			broken[bors..num..ltr] = true
		end
	end

	for i = 2, string.len(s)+1 do
		local chr = string.sub(s, i, i)
		
		local c2num = tonumber(chr)
		if c2num then
			--if the numbers are adjacent go back and add transitions
			if not has_letters then
				add_all(num)
			end

			num = c2num
			negative, has_letters = false, false
		elseif chr == "-" then
			add_all(num) --subtract later
			negative = true
		elseif chr == "/" or chr == "" then
			--kind of a hack to fix cases where a section ends with a number. this is why the loop runs for +1 iterations btw
			if not has_letters then
				add_all(num)
			end
			has_letters = true --stops it from adding to survival as well
		elseif chr == "S" then
			bors = "S"
		else --if chr is a letter
			local trans = bors..num..chr
			if not negative then
				broken[trans] = true
			else
				broken[trans] = nil
			end
			has_letters = true
		end
	end

	return broken
end

function make_rule(b) --converts a table of transitions into a rulestring
	local str = ""
	local decomp = {B0 = {}, B1 = {}, B2 = {}, B3 = {}, B4 = {}, B5 = {}, B6 = {}, B7 = {}, B8 = {}, S0 = {}, S1 = {}, S2 = {}, S3 = {}, S4 = {}, S5 = {}, S6 = {}, S7 = {}, S8 = {}}

	for trans, _ in pairs(b) do
		local group, chr = string.sub(trans, 1, 2), string.sub(trans, 3, 3)
		table.insert(decomp[group], chr)
	end

	for _, bors in pairs({"B", "S"}) do
		str = str .. bors

		for num = 0, 8 do
			local current, total = decomp[bors .. num], configs[num]
			if #current ~= 0 then
				str = str .. num
				
				if #current == #total then
					--this line intentionally left blank
				else
					if #current <= math.ceil(#total / 2) then
						table.sort(current)
						for _, chr in pairs(current) do
							str = str .. chr
						end
					else
						str = str .. "-"

						--create a dict version of current
						local cur_dict = {}
						for _, chr in pairs(current) do
							cur_dict[chr] = true
						end

						--now compare against total
						for _, chr in ipairs(total) do
							if cur_dict[chr] == nil then
								str = str .. chr
							end
						end
					end
				end
			end
		end

		if bors == "B" then
			str = str .. "/"
		end
	end

	return str
end

----borrowing end

local base_rule, min_rule, max_rule = break_rule(g.getrule()), break_rule("B/S"), break_rule("B12345678/S012345678")

local gens = tonumber(g.getstring("What generation do you want to keep up to?", "100", "ranger.lua"))

function step(gen)
	local rect = g.getrect()
	--loop through top-right corners
	for y = rect[2]-2, rect[2] + rect[4]-1 do
		for x = rect[1]-2, rect[1] + rect[3]-1 do
			local on_count = 0
			local score = 1

			--loop through offsets
			for oy = 0, 2 do
				for ox = 0, 2 do
					local ex, ey = x + ox, y + oy
					local power = oy * 3 + ox
					if g.getcell(ex, ey) == 1 then
						on_count = on_count + 1
						score = score + math.floor(2 ^ power)
					end
				end
			end

			local bors = "B"
			if g.getcell(x+1, y+1) == 1 then
				bors = "S"
				on_count = on_count - 1
			end

			if base_rule.B0 == true and gen % 2 == 1 then
				--invert everything
				if bors == "B" then
					bors = "S"
				else
					bors = "B"
				end
				on_count = 8 - on_count
				score = 513 - score --only matters for B4
			end

			local letter = string.sub(letters, score, score)
			if letter == "." then --0 or 8
				letter = ""
			end

			local key = bors .. on_count .. letter
			min_rule[key], max_rule[key] = base_rule[key], base_rule[key]
		end
	end
end

for gen = 0, gens do
	if g.empty() == true then
		break
	end
	step(gen)
	g.run(1)
end

g.reset()


min_rule.B0, max_rule.B0 = base_rule.B0, base_rule.B0 --exception for patterns which evaded B0 detection (perhaps by being square and dense)
min_rule, max_rule = make_rule(min_rule), make_rule(max_rule)

local range_string = min_rule .. " - " .. max_rule
g.getstring("The pattern works in the range [" .. range_string .. "].\n\nCopy it from the box below if you would like.", range_string, "ranger.lua")
The code which handles rulestrings is borrowed from my other script, lms.lua.

There's some optimizations I could make to this code (only checking the neighbourhoods of live cells, for one) but anything with a high enough period/size to need those optimizations is likely already endemic.
That's the bunny. (she/her)

Post Reply