--[[ 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()