--[[ This script lets you use Golly to explore BSFKL rules as defined by Brian Prentice here: http://www.conwaylife.com/forums/viewtopic.php?f=11&t=1381#p12223 Author: Andrew Trevorrow (andrew@trevorrow.com), May 2019. --]] local g = golly() local gp = require "gplus" local split = gp.split require "gplus.NewCA" SCRIPT_NAME = "BSFKL" DEFAULT_RULE = "B1S235F23K4L035" RULE_HELP = [[ This script supports rule strings of the form BbSsFfKkLl where b, s, f, k and l are multiple digits in the range 0 to 8. Rules with both B0 and F0 are not currently supported.
Here is Brian Prentice's description of this CA:
A cell can be in one of three states: live, dead or destructive.
A live cell dies if it has k destructive neighbors.
If a live cell does not have k destructive neighbors but does have s live neighbors it remains alive.
If a live cell has neither k destructive neighbors nor s live neighbors it becomes a destructive cell.
A destructive cell dies if it has l live neighbors and remains a destructive cell otherwise.
A cell is born if it has f destructive neighbors and b live neighbors.
]]
-- the following are non-local so a startup script can change them
DEFWD, DEFHT = 500, 500 -- default grid size
aliases = {
B2SK1F2 = "B2SF01K12345678L12345678", -- original rule by c0b0p0
B2SF2K1 = "B2SF01K12345678L12345678", -- ditto but order changed
}
--------------------------------------------------------------------------------
-- the following BSFKL values are set by ParseRule and used in NextPattern
local bvals, svals, fvals, kvals, lvals
function ParseRule(newrule)
-- Parse the given rule string.
-- If valid then return nil, the canonical rule string,
-- the width and height of the grid, and the number of states.
-- If not valid then just return an appropriate error message.
if #newrule == 0 then
newrule = DEFAULT_RULE -- should be a valid rule!
else
-- check for a known alias
local rule = aliases[newrule]
if rule then
newrule = rule
elseif newrule:find(":") then
-- try without the suffix
local p, s = split(newrule,":")
rule = aliases[p]
if rule then newrule = rule..":"..s end
end
end
local prefix, suffix = split(newrule:upper(),":")
-- check for a valid prefix
local b, s, f, k, l = prefix:match("^B([0-8]*)S([0-8]*)F([0-8]*)K([0-8]*)L([0-8]*)$")
if not (b and s and f and k and l) then
return "Rule syntax is BnSnFnKnLn where n has any digits from 0 to 8."
end
-- check for a valid suffix like T50 or T50,30
local wd, ht = DEFWD, DEFHT
if suffix then
if suffix:find(",") then
wd, ht = suffix:match("^T(%d+),(%d+)$")
else
wd = suffix:match("^T(%d+)$")
ht = wd
end
wd = tonumber(wd)
ht = tonumber(ht)
if wd == nil or ht == nil then
return "Rule suffix must be Twd,ht or Twd."
end
end
if wd < 10 then wd = 10 elseif wd > 2000 then wd = 2000 end
if ht < 10 then ht = 10 elseif ht > 2000 then ht = 2000 end
if b:find("0") and f:find("0") then
return "B0 with F0 is not supported."
-- we could allow B0 and F0 but we'd need to write a very slow version of NextPattern
end
-- given rule is valid
-- set bvals, svals, fvals, kvals, lvals for use in NextPattern
bvals = {}
svals = {}
fvals = {}
kvals = {}
lvals = {}
local bc, sc, fc, kc, lc = "", "", "", "", "" -- canonical digit strings
for i = 0, 8 do
if b:find(tostring(i)) then bvals[i] = 1; bc = bc..i end
if s:find(tostring(i)) then svals[i] = 1; sc = sc..i end
if f:find(tostring(i)) then fvals[i] = 1; fc = fc..i end
if k:find(tostring(i)) then kvals[i] = 1; kc = kc..i end
if l:find(tostring(i)) then lvals[i] = 1; lc = lc..i end
end
-- create the canonical form of the given rule
local canonrule = "B"..bc.."S"..sc.."F"..fc.."K"..kc.."L"..lc..":T"..wd..","..ht
return nil, canonrule, wd, ht, 3
end
--------------------------------------------------------------------------------
function NextPattern(currcells, minx, miny, maxx, maxy)
-- Create the next pattern using the given parameters:
-- currcells is a non-empty cell array containing the current pattern.
-- minx, miny, maxx, maxy are the cell coordinates of the grid edges.
-- This function must return the new pattern as a cell array.
local newcells = {} -- cell array for the new pattern (multi-state)
local newlen = 0 -- length of newcells
local ecount1 = {} -- empty neighbor counts for state 1 cells
local ecount2 = {} -- empty neighbor counts for state 2 cells
local gridwd = maxx-minx+1
local get = g.getcell
local currlen = #currcells -- currcells is multi-state so currlen is odd
if currlen % 3 > 0 then
currlen = currlen - 1 -- ignore padding int
end
for i = 1, currlen, 3 do
local x = currcells[i]
local y = currcells[i+1]
local state = currcells[i+2]
local xm1 = x-1
local xp1 = x+1
local ym1 = y-1
local yp1 = y+1
-- might need to wrap edges
if xm1 < minx then xm1 = maxx end
if xp1 > maxx then xp1 = minx end
if ym1 < miny then ym1 = maxy end
if yp1 > maxy then yp1 = miny end
-- calculate offsets needed to create keys for ecount1 and ecount2
local xm1off = xm1-minx
local xoff = x -minx
local xp1off = xp1-minx
local ym1off = (ym1-miny) * gridwd
local yoff = (y -miny) * gridwd
local yp1off = (yp1-miny) * gridwd
local keys = {
xm1off + ym1off,
xoff + ym1off,
xp1off + ym1off,
xm1off + yoff,
xp1off + yoff,
xm1off + yp1off,
xoff + yp1off,
xp1off + yp1off
}
local neighbors = { -- order must match ordering of above keys
get(xm1, ym1),
get(x , ym1),
get(xp1, ym1),
get(xm1, y ),
get(xp1, y ),
get(xm1, yp1),
get(x , yp1),
get(xp1, yp1)
}
local count1 = 0
local count2 = 0
for i = 1, 8 do
if neighbors[i] == 1 then
count1 = count1 + 1
elseif neighbors[i] == 2 then
count2 = count2 + 1
else
-- empty neighbor
local k = keys[i]
if state == 1 then
ecount1[k] = (ecount1[k] or 0) + 1
ecount2[k] = ecount2[k] or 0
else -- state == 2
ecount2[k] = (ecount2[k] or 0) + 1
ecount1[k] = ecount1[k] or 0
end
end
end
if state == 1 then
if kvals[count2] then
-- cell dies
elseif svals[count1] then
newlen = newlen+1 ; newcells[newlen] = x
newlen = newlen+1 ; newcells[newlen] = y
newlen = newlen+1 ; newcells[newlen] = 1
else
newlen = newlen+1 ; newcells[newlen] = x
newlen = newlen+1 ; newcells[newlen] = y
newlen = newlen+1 ; newcells[newlen] = 2
end
else -- state == 2
if lvals[count1] then
-- cell dies
else
newlen = newlen+1 ; newcells[newlen] = x
newlen = newlen+1 ; newcells[newlen] = y
newlen = newlen+1 ; newcells[newlen] = 2
end
end
end
-- now add births to newcells using the empty neighbor counts created above
for k,v in pairs(ecount1) do
if bvals[v] and fvals[ecount2[k]] then
newlen = newlen+1 ; newcells[newlen] = minx + (k % gridwd)
newlen = newlen+1 ; newcells[newlen] = miny + (k // gridwd)
newlen = newlen+1 ; newcells[newlen] = 1
end
end
if newlen > 0 then
-- newcells is a multi-state cell array so ensure length is odd
if newlen & 1 == 0 then newcells[newlen+1] = 0 end
end
-- delete the old pattern and add the new pattern
g.putcells(currcells, 0, 0, 1, 0, 0, 1, "xor")
g.putcells(newcells)
return newcells -- return the new pattern
end
--------------------------------------------------------------------------------
-- override the SetColors function
function SetColors()
-- this CA has 3 states
g.setcolors( {0,0,0,0} ) -- state 0 is black
g.setcolors( {1,255,255,0} ) -- state 1 is yellow
g.setcolors( {2,255,0,0} ) -- state 2 is red
end
--------------------------------------------------------------------------------
-- user's startup script might want to override this
function RandomRule()
local rand = math.random
local b,s,f,k,l = "","","","",""
for i = 0, 8 do
if rand(0,1) > 0 then b = b..i end
if rand(0,1) > 0 then s = s..i end
if rand(0,1) > 0 and not b:find("0") then f = f..i end
if rand(0,1) > 0 then k = k..i end
if rand(0,1) > 0 then l = l..i end
end
return "B"..b.."S"..s.."F"..f.."K"..k.."L"..l
end
--------------------------------------------------------------------------------
-- allow alt-R to create a random pattern with a random rule
local saveHandleKey = HandleKey
function HandleKey(event)
local _, key, mods = split(event)
if key == "r" and mods == "alt" then
SetRule(RandomRule())
RandomPattern()
else
-- pass the event to the original HandleKey
saveHandleKey(event)
end
end
--------------------------------------------------------------------------------
-- and away we go...
StartNewCA()