1--[[
2This script lets you explore rules using the Margolus neighborhood.
3Author: Andrew Trevorrow (andrew@trevorrow.com), May 2019.
4--]]
5
6local g = golly()
7local gp = require "gplus"
8local split = gp.split
9
10require "gplus.NewCA"
11SCRIPT_NAME = "Margolus"
12DEFAULT_RULE = "M0,8,4,3,2,5,9,7,1,6,10,11,12,13,14,15"     -- BBM
13RULE_HELP = [[
14This script lets you explore rules using the Margolus neighborhood.
15Rule strings are of the form Mn,n,n,n,n,n,n,n,n,n,n,n,n,n,n,n
16where there must be 16 numbers with values from 0 to 15.
17<p>
18You can also enter rules using MCell's syntax (MS,Dn;n;n;n...).
19Or you can enter one of the following aliases (case must match):
20<p>
21<center>
22<table cellspacing=1 border=2 cols=2 width="90%%">
23<tr><td align=right> Alias    &nbsp;</td><td>&nbsp; Rule </td></tr>
24<tr><td align=right> bbm      &nbsp;</td><td>&nbsp; M0,8,4,3,2,5,9,7,1,6,10,11,12,13,14,15 </td></tr>
25<tr><td align=right> critters &nbsp;</td><td>&nbsp; M15,14,13,3,11,5,6,1,7,9,10,2,12,4,8,0 </td></tr>
26<tr><td align=right> tron     &nbsp;</td><td>&nbsp; M15,1,2,3,4,5,6,7,8,9,10,11,12,13,14,0 </td></tr>
27</table>
28</center>
29<p>
30For more details about the Margolus neighborhood see this link:<br>
31<a href="http://www.mirekw.com/ca/rullex_marg.html"
32        >http://www.mirekw.com/ca/rullex_marg.html</a>
33]]
34
35-- the following are non-local so a startup script can change them
36DEFWD, DEFHT = 500, 500         -- default grid size
37aliases = {
38    bbm =      "M0,8,4,3,2,5,9,7,1,6,10,11,12,13,14,15",
39    critters = "M15,14,13,3,11,5,6,1,7,9,10,2,12,4,8,0",
40    tron =     "M15,1,2,3,4,5,6,7,8,9,10,11,12,13,14,0",
41}
42
43--------------------------------------------------------------------------------
44
45NextPattern = function() end    -- ParseRule sets this to SlowPattern or FastPattern
46local transition = {}           -- transition table set by ParseRule and used in NextPattern
47local even_transition = {}      -- transition table for even generations
48local odd_transition = {}       -- transition table for odd generations
49local alternate_rules = false   -- FastPattern uses even_transition and odd_transition?
50
51function ParseRule(newrule)
52    -- Parse the given rule string.
53    -- If valid then return nil, the canonical rule string,
54    -- the width and height of the grid, and the number of states.
55    -- If not valid then just return an appropriate error message.
56
57    if #newrule == 0 then
58        newrule = DEFAULT_RULE  -- should be a valid rule!
59    else
60        -- check for a known alias
61        local rule = aliases[newrule]
62        if rule then
63            newrule = rule
64        elseif newrule:find(":") then
65            -- try without the suffix
66            local p, s = split(newrule,":")
67            rule = aliases[p]
68            if rule then newrule = rule..":"..s end
69        end
70    end
71
72    local prefix, suffix = split(newrule:upper(),":")
73
74    -- check for a valid prefix
75    if prefix:sub(1,1) ~= "M" then
76        return "Rule must start with M."
77    end
78    local blocknum = {}
79    local i = 0
80    -- note that we append a comma to prefix to simplify the gmatch pattern
81    for n in string.gmatch(prefix..",", "(%d+)[,;]") do
82        if tonumber(n) > 15 then
83            return "Bad number: "..n.." (must be from 0 to 15)."
84        end
85        blocknum[i] = tonumber(n)
86        i = i + 1
87        if i > 16 then break end
88    end
89    if i ~= 16 then
90        return "Rule must specify 16 comma-separated numbers from 0 to 15."
91    end
92
93    -- check for a valid suffix like T50 or T50,30
94    local wd, ht = DEFWD, DEFHT
95    if suffix then
96        if suffix:find(",") then
97            wd, ht = suffix:match("^T(%d+),(%d+)$")
98        else
99            wd = suffix:match("^T(%d+)$")
100            ht = wd
101        end
102        wd = tonumber(wd)
103        ht = tonumber(ht)
104        if wd == nil or ht == nil then
105            return "Rule suffix must be Twd,ht or Twd."
106        end
107    end
108    if wd < 8 then wd = 8 elseif wd > 1000 then wd = 1000 end
109    if ht < 8 then ht = 8 elseif ht > 1000 then ht = 1000 end
110
111    -- ensure wd and ht are multiples of 4 to simplify NextPattern logic
112    wd = wd - (wd % 4)
113    ht = ht - (ht % 4)
114
115    -- given rule is valid
116
117    -- create the transition table for use in NextPattern
118    transition = {}
119    for i = 0, 15 do transition[i] = blocknum[i] end
120
121    -- create the canonical form of the given rule
122    local canonrule = "M"
123    for i = 0, 15 do
124        canonrule = canonrule..blocknum[i]
125        if i < 15 then canonrule = canonrule.."," end
126    end
127    canonrule = canonrule..":T"..wd..","..ht
128
129    if transition[0] == 0 then
130        NextPattern = FastPattern
131    else
132        NextPattern = SlowPattern
133    end
134
135    alternate_rules = false
136    if transition[0] == 15 and transition[15] == 0 then
137        -- for rules like Critters and Tron we can avoid strobing and use
138        -- FastPattern with alternating transition tables, eg:
139        --         i: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
140        --  Critters: 15,14,13,3, 11,5, 6, 1, 7, 9, 10, 2,  12, 4,  8,  0
141        -- even gens: 0, 1, 2, 12,4, 10,9, 14,8, 6, 5,  13, 3,  11, 7,  15
142        --  odd gens: 0, 8, 4, 12,2, 10,9, 7, 1, 6, 5,  11, 3,  13, 14, 15
143        alternate_rules = true
144        NextPattern = FastPattern
145        even_transition = {}
146        odd_transition = {}
147        for i = 0, 15 do
148            even_transition[i] = 15 - transition[i]     -- inverse
149            if i == transition[i] then
150                odd_transition[i] = even_transition[i]
151            elseif even_transition[i] == 0 then odd_transition[i] = 0
152            elseif even_transition[i] == 15 then odd_transition[i] = 15
153            elseif even_transition[i] == 1 then odd_transition[i] = 8
154            elseif even_transition[i] == 8 then odd_transition[i] = 1
155            elseif even_transition[i] == 2 then odd_transition[i] = 4
156            elseif even_transition[i] == 4 then odd_transition[i] = 2
157            elseif even_transition[i] == 3 then odd_transition[i] = 5
158            elseif even_transition[i] == 5 then odd_transition[i] = 3
159            elseif even_transition[i] == 6 then odd_transition[i] = 9
160            elseif even_transition[i] == 9 then odd_transition[i] = 6
161            elseif even_transition[i] == 7 then odd_transition[i] = 14
162            elseif even_transition[i] == 14 then odd_transition[i] = 7
163            elseif even_transition[i] == 10 then odd_transition[i] = 12
164            elseif even_transition[i] == 12 then odd_transition[i] = 10
165            elseif even_transition[i] == 11 then odd_transition[i] = 13
166            elseif even_transition[i] == 13 then odd_transition[i] = 11
167            end
168        end
169    end
170
171    return nil, canonrule, wd, ht, 2
172end
173
174--------------------------------------------------------------------------------
175
176function FastPattern(currcells, minx, miny, maxx, maxy)
177    -- Create the next pattern when transition[0] == 0 or when using
178    -- alternating rules.
179
180    local newcells = {}         -- cell array for the new pattern (one-state)
181    local newlen = 0            -- length of newcells
182    local blocks = {}           -- has all 2x2 blocks with at least 1 live cell
183    local gridwd = maxx-minx+1
184    local get = g.getcell
185    local oddgen = GetGenCount() & 1 == 1
186
187    if alternate_rules then
188        if oddgen then
189            transition = odd_transition
190        else
191            transition = even_transition
192        end
193    end
194
195    for i = 1, #currcells, 2 do -- currcells is a one-state cell array
196        local x = currcells[i]
197        local y = currcells[i+1]
198        -- find the top left cell in the 2x2 block to which this live cell belongs
199        local n
200        if oddgen then
201            -- odd-numbered generation so top left cell in block must have odd coords
202            local oddx = x & 1 == 1
203            local oddy = y & 1 == 1
204            if oddx and oddy then
205                n = 1
206            elseif not oddx and oddy then
207                n = 2
208                x = x-1
209                if x < minx then x = maxx end
210            elseif oddx and not oddy then
211                n = 4
212                y = y-1
213                if y < miny then y = maxy end
214            else -- x,y both even
215                n = 8
216                x = x-1
217                y = y-1
218                if x < minx then x = maxx end
219                if y < miny then y = maxy end
220            end
221        else
222            -- even-numbered generation so top left cell in block must have even coords
223            -- (note that ParseRule ensures both grid dimensions are multiples of 4,
224            -- so wrapping won't be needed and minx,miny are always even numbers)
225            local evenx = x & 1 == 0
226            local eveny = y & 1 == 0
227            if evenx and eveny then
228                n = 1
229            elseif not evenx and eveny then
230                n = 2
231                x = x-1
232            elseif evenx and not eveny then
233                n = 4
234                y = y-1
235            else -- x,y both odd
236                n = 8
237                x = x-1
238                y = y-1
239            end
240        end
241        -- x,y is now the top left cell in a non-empty 2x2 block
242        local k = (y-miny) * gridwd + (x-minx)
243        blocks[k] = (blocks[k] or 0) + n
244    end
245
246    -- go thru all the 2x2 blocks created above and put the transitions in newcells
247    for k,v in pairs(blocks) do
248        local x = minx + (k % gridwd)
249        local y = miny + (k // gridwd)
250        -- x,y is the top left cell in this 2x2 block
251        local xp1 = x+1
252        local yp1 = y+1
253        if oddgen then
254            -- may need to wrap right and bottom edges
255            if xp1 > maxx then xp1 = minx end
256            if yp1 > maxy then yp1 = miny end
257        end
258        local n = transition[v]
259        if n > 0 then
260            if n & 1 == 1 then
261                -- n is 1,3,5,7,9,11,13,15
262                newlen = newlen+1 ; newcells[newlen] = x
263                newlen = newlen+1 ; newcells[newlen] = y
264            end
265            if n == 2 or n == 3 or n == 6 or n == 7 or
266               n == 10 or n == 11 or n == 14 or n == 15 then
267                newlen = newlen+1 ; newcells[newlen] = xp1
268                newlen = newlen+1 ; newcells[newlen] = y
269            end
270            if (n >= 4 and n <= 7) or (n >= 12 and n <= 15) then
271                newlen = newlen+1 ; newcells[newlen] = x
272                newlen = newlen+1 ; newcells[newlen] = yp1
273            end
274            if n >= 8 then
275                newlen = newlen+1 ; newcells[newlen] = xp1
276                newlen = newlen+1 ; newcells[newlen] = yp1
277            end
278        end
279    end
280
281    -- delete the old pattern and add the new pattern
282    g.putcells(currcells, 0, 0, 1, 0, 0, 1, "xor")
283    g.putcells(newcells)
284
285    return newcells     -- return the new pattern
286end
287
288--------------------------------------------------------------------------------
289
290function SlowPattern(currcells, minx, miny, maxx, maxy)
291    -- Create the next pattern when transition[0] > 0 and we have to examine
292    -- every cell in the grid (so very slow).
293
294    local newcells = {}         -- cell array for the new pattern (one-state)
295    local newlen = 0            -- length of newcells
296    local get = g.getcell
297    local oddgen = GetGenCount() & 1 == 1
298
299    local xfirst, yfirst, xlast, ylast
300    if oddgen then
301        -- odd-numbered generation
302        xfirst = minx+1
303        yfirst = miny+1
304        xlast = maxx
305        ylast = maxy
306    else
307        -- even-numbered generation (note that ParseRule ensures both grid
308        -- dimensions are multiples of 4, so wrapping won't be needed and
309        -- minx,miny are always even numbers)
310        xfirst = minx
311        yfirst = miny
312        xlast = maxx-1
313        ylast = maxy-1
314    end
315
316    for y = yfirst, ylast, 2 do
317        local yp1 = y+1
318        -- may need to wrap bottom edge
319        if oddgen and yp1 > maxy then yp1 = miny end
320        for x = xfirst, xlast, 2 do
321            local xp1 = x+1
322            -- may need to wrap right edge
323            if oddgen and xp1 > maxx then xp1 = minx end
324            local i = get(x, y)
325            i = i + 2 * get(xp1, y)
326            i = i + 4 * get(x,   yp1)
327            i = i + 8 * get(xp1, yp1)
328            local n = transition[i]
329            if n > 0 then
330                if n & 1 == 1 then
331                    -- n is 1,3,5,7,9,11,13,15
332                    newlen = newlen+1 ; newcells[newlen] = x
333                    newlen = newlen+1 ; newcells[newlen] = y
334                end
335                if n == 2 or n == 3 or n == 6 or n == 7 or
336                   n == 10 or n == 11 or n == 14 or n == 15 then
337                    newlen = newlen+1 ; newcells[newlen] = xp1
338                    newlen = newlen+1 ; newcells[newlen] = y
339                end
340                if (n >= 4 and n <= 7) or (n >= 12 and n <= 15) then
341                    newlen = newlen+1 ; newcells[newlen] = x
342                    newlen = newlen+1 ; newcells[newlen] = yp1
343                end
344                if n >= 8 then
345                    newlen = newlen+1 ; newcells[newlen] = xp1
346                    newlen = newlen+1 ; newcells[newlen] = yp1
347                end
348            end
349        end
350    end
351
352    -- delete the old pattern and add the new pattern
353    g.putcells(currcells, 0, 0, 1, 0, 0, 1, "xor")
354    g.putcells(newcells)
355
356    return newcells     -- return the new pattern
357end
358
359--------------------------------------------------------------------------------
360
361-- override the SetColors function
362function SetColors()
363    g.setcolors( {0,0,0,0} )        -- state 0 is black
364    g.setcolors( {1,255,255,0} )    -- state 1 is yellow
365end
366
367--------------------------------------------------------------------------------
368
369-- user's startup script might want to override this
370function RandomRule()
371    local rand = math.random
372    local rule = "M0,"  -- avoid strobing rules
373    for i = 1, 14 do
374        rule = rule..rand(0,15)..","
375    end
376    return rule..rand(0,15)
377end
378
379--------------------------------------------------------------------------------
380
381-- allow alt-R to create a random pattern with a random rule
382local saveHandleKey = HandleKey
383function HandleKey(event)
384    local _, key, mods = split(event)
385    if key == "r" and mods == "alt" then
386        SetRule(RandomRule())
387        RandomPattern()
388    else
389        -- pass the event to the original HandleKey
390        saveHandleKey(event)
391    end
392end
393
394--------------------------------------------------------------------------------
395
396-- and away we go...
397StartNewCA()
398