ConwayLife.com - A community for Conway's Game of Life and related cellular automata
Home  •  LifeWiki  •  Forums  •  Download Golly

Golly scripts

For scripts to aid with computation or simulation in cellular automata.

Golly scripts

Postby Nathaniel » March 25th, 2009, 9:52 pm

If you have any custom Golly python or perl scripts, feel free to post them here. The first one is a blazingly-fast version of the "goto.py" script that comes with Golly:

gofast.py
#Golly Python script
#Written by PM 2Ring 2009.3.24

''' Advance generation count using binary hashing '''

from glife import getstring, validint
import golly as g

def intbits(n):
  ''' Convert integer n to a bit list '''
  bits = []
  while n:
    bits += [n & 1]
    n >>= 1
  return bits   
   
def gofast(num):
  ''' Fast goto '''
  #Save current settings 
  oldbase = g.getbase()
  oldstep = g.getstep()
  oldhash = g.setoption("hashing", True)
 
  g.show('Hit ESC to abort')

  #Advance by binary powers, to maximize advantage of hashing 
  g.setbase(2)
  for i, b in enumerate(intbits(num)):
    if b:
      g.setstep(i)
      g.step()
      g.dokey(g.getkey())
      g.update()
      if golly.empty():
        break
       
  g.show('')
 
  #Restore settings 
  g.setoption("hashing", oldhash)
  g.setbase(oldbase)
  g.setstep(oldstep)
     
def main():
  numstr = getstring('How many gens to advance? :')
  if not validint(numstr):
    g.exit('Bad number: %s' % numstr)
  num = int(numstr)
  if num<1:
    g.exit('Number must be positive!')
  gofast(num)

main()
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby PM 2Ring » March 26th, 2009, 11:42 am

Here's a slightly faster version of gofast.py. This one lets you choose the base size, so it's a little less compatible with the standard goto.py. To get a brief help message, press Enter at the prompt. When it's finished generating, it shows how many seconds it took in the status line.

gofast.py.
#Golly Python script
#Written by PM 2Ring 2009.3.26

''' Advance generation count using hashing '''

from glife import getstring, validint
import golly as g
from time import time

def intbase(n, b):
  ''' Convert integer n>=0 to a base b digit list '''
  digits = []
  while n:
    digits += [n % b]
    n //= b
  return digits or [0]   
   
def gofast(num, base=2):
  ''' Fast goto '''
  #Save current settings 
  oldbase = g.getbase()
  oldstep = g.getstep()
  oldhash = g.setoption("hashing", True)
 
  g.show('Hit ESC to abort')

  #Advance by powers of base, using hashing. Works best if base is a power of 2. 
  g.setbase(base)
  #print 'steps=', num, 'base=', base
  secs = time()
  for i, d in enumerate(intbase(num, base)):
    if d:
      #print i, d
      g.setstep(i)
      for j in xrange(d):
        g.step()
      if golly.empty():
        break
      g.update()
      g.dokey(g.getkey())       
     
  secs = '%.3f secs' % (time() - secs) 
  g.show(secs)
  #print secs       
 
  #Restore settings 
  g.setoption("hashing", oldhash)
  g.setbase(oldbase)
  g.setstep(oldstep)
 
class ArgError(Exception): pass 
     
def main():
  defbase = 8
  prompt = 'Enter number of generations to advance and \
optional base (default=%d)' % defbase
  usage = 'Usage: %s, separated by space. \
Base should be a binary power for highest speed.' % prompt

  #Loop until we get correct args, or are aborted by ESC   
  while True:
    try:
      args = getstring(prompt + ' :').split()
      if args == []:
        raise ArgError
     
      for s in args: 
        if not validint(s):
          raise ArgError, '[%s] is not a valid integer.' % s     

      #Convert arguments to integer, stripping commas
      args = [int(s.replace(',', '')) for s in args]           
      num = args[0]
      if num<1:
        raise ArgError, 'Number to advance by must be positive, not %d!' % num
     
      base = len(args) > 1 and args[1] or defbase
      if base<2:
        raise ArgError, 'Base must be 2 or greater, not %d' % base
           
    except ArgError, s:
      g.warn('%s\n\n%s' % (s, usage)) 
    else:   
      break 
         
  gofast(num, base)

main()
Last edited by PM 2Ring on April 3rd, 2009, 1:02 pm, edited 1 time in total.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » March 26th, 2009, 11:49 am

Here's a preliminary version of a script that converts the current selection into a pattern made of gliders (or blocks, snakes or whatever). Currently, the tiling pattern is hard coded into the script, but it could easily come from the clipboard, or be loaded via a "Load pattern" dialog. Suggestions are welcome!

#Golly Python script
#Written by PM 2Ring 2009.3.23

''' Convert cells in selected pattern into tiles '''

from glife import *
import golly

def tile(pat, padx=0, pady=0):
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("There is no selection.")
 
  #Make top left corner of selection the origin of the tiled pattern   
  x, y = selrect[:2] 
  cells = golly.getcells(selrect) 
  cellsxy = [(cells[i]-x, cells[i+1]-y) for i in xrange(0, len(cells), 2)]
 
  #Make tiled pattern in a new layer
  golly.duplicate()
  #golly.setoption("syncviews", False)

  #Build new window title from old 
  title = golly.getname().split('.')[0]
  title = '%s_tiled' % title 
  golly.new(title) 

  #Get tile pattern size & add padding   
  px, py = getminbox(pat)[2:]
  px, py = px + padx, py + pady
 
  all = pattern()
  for x, y in cellsxy:
    all += pat(x*px, y*py)
    #pat.put(x*px, y*py)
   
  all.display()   
     
def main():
  snakes = pattern('2obo$ob2o2$ob2o$2obo!')
  block =  pattern('2o$2o!')
  blocks = pattern('2ob2o$2ob2o$5b$2ob2o$2ob2o!')
  glider = pattern('bob$2bo$3o!')
   
  #tile(snakes, 2, 1) 
  #tile(block, 2, 2) 
  #tile(blocks, 2, 2) 
  tile(glider, 2, 2) 

main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby Nathaniel » March 28th, 2009, 4:38 pm

I got sick of calculating it by hand, so I made a Python script that calculates the heat of the current pattern (oscillator or spaceship) over a user-specified number of generations. It also outputs the maximum and minimum changes that were noticed when going from one generation to the next.

heat.py
#Golly Python script
#Written by Nathaniel Johnston
#March 28, 2009

''' Calculate the heat of the current oscillator/spaceship '''       

from glife import getstring, validint
import golly as g

def chunks(l,w):
    for i in xrange(0, len(l), 2):
        yield l[i]+(w*l[i+1])

if g.empty(): g.exit("The pattern is empty.")
s = g.getstring("Enter the period:","", "Heat calculator")

if not validint(s):
  g.exit('Bad number: %s' % s)
numsteps = int(s)
if numsteps < 2:
  g.exit('Period must be at least 2.')

g.show('Processing...')

heat = 0;
maxheat = 0;
minheat = 9*g.getpop();

for i in range(0, numsteps):
  bb = g.getrect()
  clist = list(chunks(g.getcells(bb), bb[2]+2))
  g.run(1)
  dlist = list(chunks(g.getcells(g.getrect()), bb[2]+2))
  theat = (len(clist)+len(dlist)-2*len([x for x in set(clist).intersection( set(dlist) )]))
  heat += theat
  maxheat = max(theat, maxheat)
  minheat = min(theat, minheat)

g.show('Heat: %.4f, Max change: %d, Min change: %d' % ((float(heat)/numsteps), maxheat, minheat))
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby PM 2Ring » April 3rd, 2009, 12:57 pm

Here's an improved version of the pattern tiling script. It now uses the current clipboard pattern.

#Golly Python script
#Written by PM 2Ring 2009.3.23
 
''' Use the current selection as a template to create a tiled pattern, converting
each live cell in the selection into a copy of the clipboard pattern. Each dead cell
in the selection becomes a rectangle the same size as the clipboard pattern. '''

from glife import *
import golly

def tile(pat, px, py):
  ''' Convert live cells in selected pattern into copies of the pattern pat, size px*px.
The tile pattern is in a new layer. '''
 
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("There is no selection.")
 
  #Make top left corner of selection the origin of the tiled pattern   
  x, y = selrect[:2] 
  cells = golly.getcells(selrect) 
  cellsxy = [(cells[i]-x, cells[i+1]-y) for i in xrange(0, len(cells), 2)]
 
  #Make tiled pattern in a new layer
  golly.duplicate()
  #golly.setoption("syncviews", False)

  #Build new window title from old 
  title = golly.getname().split('.')[0]
  title = '%s_tiled' % title 
  golly.new(title) 
 
  all = pattern()
  for x, y in cellsxy:
    all += pat(x*px, y*py)
    #pat.put(x*px, y*py)
   
  all.display()   
     
def main():
  cliplist = golly.getclip()  # 1st 2 items are width, height
  tile(pattern(cliplist[2:]), *cliplist[0 : 2]) 

main()


The following script builds n-bit binary counters from p46 guns, using snake & boat bits. It uses the early version of my gofast routine to initialize the counter. Try pressing TAB to watch the counting process.

#Golly Python script
#Written by PM 2Ring 2009.3.23

''' Build a multi-stage binary counter from p46 guns '''

from glife import *
import golly

def parts():
  higun = pattern('''x = 13, y = 49, rule = B3/S23
8b2o$2bo5b2o$b3o$o3bo$2ob2o2$b3o$b3o6$3b2ob2o$3b2ob2o$3b2ob2o$bob2ob2o
bo$b3o3b3o$2bo5bo12$4b3o3b3o$4bo2bobo2bo$4bo7bo2$5bo5bo$6b2ob2o12$4b2o
5b2o$4b2o5b2o!''', 36, -51)
  logun = pattern('''x = 13, y = 49, rule = B3/S23
11b2o$5bo5b2o$4b3o$3bo3bo$3b2ob2o2$4b3o$4b3o6$6b2ob2o$6b2ob2o$6b2ob2o$
4bob2ob2obo$4b3o3b3o$5bo5bo13$b3o3b3o$o2bo3bo2bo$4bobo$4bobo$4bobo$o2b
o3bo2bo$bo7bo4$2bo$2ob2o2$2ob2o$o3bo$b3o$8b2o$8b2o!''', 31, 9)
  snake = pattern('''ob2o$2obo!''') 
  return higun, logun, snake
 
def bitcounters(num):
  ''' Build & display bit counters '''   
  bnum = 2 ** max(0, num-3)
  glen = 23 * bnum
  DX = 2*glen - 3

  #Build counters 
  higun, logun, snake = parts()
  counterMSB = snake + logun(glen, glen)
  counter = counterMSB + higun

  #Place all counters 
  counterMSB.put(0, 0) 
  for i in xrange(1, num): 
    counter.put(i*DX, 0)
   
  #Calculate how many gun periods are needed to initialize
  return 2 * ((2 * num + 1) * bnum + 1)

def intbits(n):
  ''' Convert integer n to a bit list '''
  bits = []
  while n:
    bits += [n & 1]
    n >>= 1
  return bits   
   
def make_counters():
  numstr = getstring('How many bits? [1-20] :')
  if not validint(numstr):
    golly.exit('Bad bit number: %s' % numstr)
  num = int(numstr)
  if num<1 or num>20:
    golly.exit('Bit number out of range: %d' % num)
         
  title = '%d_bitcounter.rle' % num   
  golly.new(title)

  #Put counters, & get how many gun periods are needed to initialize   
  cycles = bitcounters(num)
  steps = cycles * 46 
  #print 'cycles = ', cycles, 'steps =', steps

  golly.fit()
  golly.setcursor(3) 
  golly.update()
   
  golly.show('Initializing, press ESC to abort')

  #Fast goto       
  golly.setoption("hashing", True)
  golly.setbase(2)
  for i, b in enumerate(intbits(steps)):
    if b:
      golly.setstep(i)
      golly.step()
      golly.update()
      golly.dokey( golly.getkey() ) 
 
  #golly.setoption("hashing", False)
  golly.setbase(46)
  golly.setstep(1)
  golly.show('') 
     
def main():
  make_counters()

main()


Here's a slightly modified version of the standard shift.py script, which copies the current selection without deleting it.

# Shift current selection by given x y amounts using optional mode.
# Author: Andrew Trevorrow (andrew@trevorrow.com), June 2006.
# Updated to use exit command, Nov 2006.

#modified to not clear original selection, Mar 2009. PM 2Ring

from glife import getstring, validint, inside
from string import lower
import golly as g

selrect = g.getselrect()
if len(selrect) == 0: g.exit("There is no selection.")

answer = getstring("Enter x y amounts and optional mode (default is or):")
xym = answer.split()

# extract x and y amounts
if len(xym) == 0: g.exit()
if len(xym) == 1: g.exit("Supply x and y amounts separated by a space.")
if not validint(xym[0]): g.exit("Bad x value: " + xym[0])
if not validint(xym[1]): g.exit("Bad y value: " + xym[1])
x = int(xym[0])
y = int(xym[1])

# extract optional mode
if len(xym) > 2:
   mode = lower(xym[2])
   if mode=="c": mode="copy"
   if mode=="o": mode="or"
   if mode=="x": mode="xor"
   if not (mode == "copy" or mode == "or" or mode == "xor"):
      g.exit("Unknown mode: " + xym[2] + " (must be copy/or/xor)")
else:
   mode = "or"

# this method cuts the current selection and pastes it into the
# new position (without changing the current clipboard pattern)
selcells = g.getcells(selrect)
#g.clear(inside)
selrect[0] += x
selrect[1] += y
g.select(selrect)
if mode == "copy":
   g.clear(inside)
g.putcells(selcells, x, y, 1, 0, 0, 1, mode)

if not g.visrect(selrect): g.fitsel()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » May 3rd, 2009, 11:36 am

Magic Spaceship gun

I like constructing glider circuits, and often need streams of gliders & other spaceships. The script below makes it easy to generate these. It works on any type of space ship. Just select a spaceship & invoke the script. It'll ask you for the stream period & the total number of ships in the stream (including the one you selected). It just uses the Golly evolve() function in a loop, so it's not very efficient, but I find it fast enough. It can also create streams of multiple ships at once.

It can also be used on non-moving oscillators, to show all cells that become live over the oscillator period.

ssstream.py
# Golly python script.
# Written by PM 2Ring, April 2009

''' Create a spaceship stream of any period.

The pattern is built from the current selection, which is assumed to be a spaceship.
'''

from glife import *
import golly
 
class ArgError(Exception): pass 

def getargs():
  ''' Get input data '''
 
  prompt = 'Enter period & number of spaceships'
  usage = 'Create a stream of the selected spaceship.\n' + prompt + ', separated by spaces.'
 
  #Loop until we get correct args, or are aborted by ESC   
  while True:
    try:
      args = getstring(prompt + ' :').split()
      if args == []:
        raise ArgError
     
      if len(args) < 2:
        raise ArgError, 'Not enough data.'
     
      #Check that args are valid integers
      for s in args: 
        if not validint(s):
          raise ArgError, '[%s] is not a valid integer.' % s     

      #Convert arguments to integer
      args = [int(s) for s in args]           
      for i in args:
        if i<=0:
          raise ArgError, 'Data must be > 0, not %d!' % i
       
    except ArgError, s:
      g.warn('%s\n\n%s' % (s, usage)) 
    else:   
      break 
  return args 
   
def main():
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("No selection")
  ss = pattern(golly.getcells(selrect))

  #Get input data
  period, num = getargs() 

  all = pattern() 
  for i in xrange(1, num):
    all += ss[i * period]
  all.put()
   
if __name__ == '__main__':
  main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » May 3rd, 2009, 11:51 am

I recently noticed that the Golly clipboard doesn't hold patterns in bitmap form - it uses RLE. So you can just copy a pattern in Golly & paste it directly into your text editor without having to save the selection in a temporary file. This can be very handy when writing scripts.

On a related note, the "Open Pattern" menu item happily opens Python scripts. This is used by a couple of the patterns that come supplied with Golly.

I'm currently working on a script that trawls through a directory (& any subdirectories) looking for any Life pattern files that Golly knows how to load & builds a set of HTML pages (one per directory) containing all the comments it finds in these pattern files, as well as the patterns themselves in the same format used by the Life Lexicon (if they aren't too wide). Clicking on the filename opens a new page with the contents of the pattern file, which is handy for the larger patterns.

I also have a simple bash script that uses sed to extract all the comments in a pattern directory tree into a single text file, if anyone's interested.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » June 9th, 2009, 5:46 am

Here's another glider stream generator. It only does gliders (unlike the script above), but it generates them much more efficiently, so it's good for making streams of thousands of gliders. The stream is loaded relative to the top left corner of the current selection. The glider direction can be specified in upper or lower case.

gliderstream1.py
# Golly python script.
# Written by PM 2Ring, April 2009

''' Create a glider stream of any period.

The stream is loaded relative to the top left corner of the current selection.
'''

from glife import *
import golly
 
class ArgError(Exception): pass 

def getargs():
  ''' Get input data '''
 
  prompt = 'Enter glider direction, period & number of gliders'
  usage = prompt + ', separated by spaces. Direction is one of NE, SE, NW or SW'
       
  directions = {'NE':(1,-1), 'SE':(1,1), 'NW':(-1,-1), 'SW':(-1,1)}
 
  #Loop until we get correct args, or are aborted by ESC   
  while True:
    try:
      args = getstring(prompt + ' :').split()
      if args == []:
        raise ArgError
     
      if len(args) < 3:
        raise ArgError, 'Not enough data.'

      #Convert direction string to a transform
      s = args[0].upper()
      if s not in directions:
        raise ArgError, '%s is not a valid direction.' % s       
      args[0] = directions[s]       
     
      #Check that 2nd & 3rd args are valid integers
      for s in args[1:]: 
        if not validint(s):
          raise ArgError, '[%s] is not a valid integer.' % s     

      #Convert arguments to integer
      args[1:] = [int(s) for s in args[1:]]           
      for i in args[1:]:
        if i<=0:
          raise ArgError, 'Data must be > 0, not %d!' % i
       
    except ArgError, s:
      g.warn('%s\n\n%s' % (s, usage)) 
    else:   
      break 
  return args 
   
def main():
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("Select a location for the glider stream.")

  #Origin         
  x, y = selrect[:2]

  #Get input data
  args = getargs()
  direction, period, num = args 
 
  transforms = {(1,-1):flip_y, (1,1):identity, (-1,-1):flip, (-1,1):flip_x}
  glider = pattern('bo$2bo$3o!', x, y, A=transforms[direction])   

  dx, dy = direction 
  all = pattern()
  for i in xrange(0, period * num, period):
    c = i // 4 
    all += glider[i % 4](c*dx, c*dy)
  all.put()
   
if __name__ == '__main__':
  main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » June 10th, 2009, 5:17 am

Here's the script I used to create the Marilyn Monroe ticker in the Patterns forum. I was going to write some docs for it, but hopefully, that won't be necessary. :) It uses the current selection as the bitmap source. The ticker is created in a new layer.

I hope this script is readable by others & the comments aren't too cryptic. If anybody would like me to write a "how it works" doc, just let me know.

bm9.py
# Golly Python script. Written by PM 2Ring, March 2009.

''' Create a 'bitmap printer', based on the golly logo ticker.
  Uses the current selection as the bitmap source.
'''

import golly
from glife import *

def get_bitmap():
  ''' Get current selection & turn it into a bitmap. ''' 
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("There is no selection, aborting.")
   
  #Get bitmap size   
  w, h = selrect[2:]
  #Adjust width, so it's in the form of 4m+1, m>=1
  w = max(w + 2, 4) // 4 * 4 + 1
  #w += 4  #Padding
  #print w
 
  #Initialize empty bitmap
  row = w * [0]
  bm = [row[:] for i in xrange(h)]

  #Populate bitmap with cell data   
  u, v = selrect[:2]
  cells = golly.getcells(selrect) 
  cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
  for x, y in cellsxy:
    bm[y][x] = 1 
  return bm

def gliders():
  ''' Glider loop parts ''' 
  #Basic glider, pointing south-west
  g0 = pattern('bob$o$3o!')
  glist = [g0.evolve(i) for i in xrange(4)]

  #Required glider configurations,
  #aligned so top left corner of bounding box is at 0,0 
  g0y   = glist[0](0, 2, flip_y)
  g0xy  = glist[0](2, 2, flip)
  g1    = glist[1](0, -1) 
  g2y   = glist[2](0, 3, flip_y)
  g2xy  = glist[2](2, 3, flip)
  g3    = glist[3](1, -1) 
  g3x   = glist[3](1, -1, flip_x)

  #Order as used in Line Makers. 0, 1, (2, 1), 3, 4, 6, (5, 6)   
  gg = [g3x, g0xy, g2xy, g0y, g2y, g3, g1] 
  #for i,glider in enumerate(gg): glider.put(10*i, 0)
  return gg   
   
def linemaker():
  ''' Line Maker parts '''
  LMTop = pattern('''x = 40, y = 44
  8bo$9bo3bo$2o2b2o8bo12b2o$2o2bo5b2o2bo12b2o$4bobo5b2o$5b2o3b3o2$5b2o3b
  3o$4bobo5b2o$4bo5b2o2bo$4b2o8bo$9bo3bo$8bo2$37b2o$37b2o7$30b3o3b3o$30b
  o2bobo2bo$29bo3bobo3bo$29b4o3b4o$30bo7bo7$37bo$36b3o$35bo3bo$35b2ob2o$
  35b2ob2o2$35b2ob2o$35b2ob2o$30b2o3bo3bo$30b2o4b3o$37bo!''')
     
  LMBase = pattern('''x = 90, y = 88
  8b2o$8b2o13$b2o5b2o$o2bo3bo2bo$bo2bobo2bo$4bobo$2b3ob3o$3o5b3o$2o7b2o$
  2o7b2o$bob2ob2obo$b3o3b3o$2bo5bo$10bo5bo$9b3o3b3o$9bob2ob2obo$8b2o7b2o
  $8b2o7b2o$8b3o5b3o$10b3ob3o$12bobo$9bo2bobo2bo$8bo2bo3bo2bo$9b2o5b2o
  10$28b2o$28b2o2$16b2o$16b2o8$87b2o$30bo3bo52b2o$30bo3bo3$27b2obo3bob2o
  $28b3o3b3o43b3o3b3o$29bo5bo44bo2bobo2bo$79bo3bobo3bo$80bo2bobo2bo$82bo
  3bo$80b2o5b2o$79b3o5b3o$79b3o5b3o2$35b2o$35b2o2$80b3o$80b3o$79b5o$78b
  2o3b2o$78b2o3b2o4$78b2o3b2o$78b2o3b2o2b2o$79b5o3b2o$80b3o$80b3o!''')
  return LMTop, LMBase

def dmprinter(pdy, copies=1):
  ''' Generate & display a dot matrix printer for named bitmap pattern '''
  #Horizontal pixel separation between Line Makers. minimum = 4
  LMsx = 4
 
  #Horizontal distance between bitmap pixels. Constant, due to LineMaker period
  pdx = 23         
   
  #Distance between Line Makers
  LMdx, LMdy = -LMsx * pdx, pdy
 
  #Get Line Maker parts 
  LMTop, LMBase = linemaker()
 
  #Eaters facing south-east & north-east
  eaterSE = pattern('2o$bo$bobo$2b2o!')
  eaterNE = eaterSE(0, 10, flip_y)
  y = 74
  eaters = (eaterNE(0, y), eaterSE(0, y))

  #Get bitmap pattern from current selection.
  bm = get_bitmap()
 
  #Make printer in a new layer
  golly.duplicate()
  golly.setoption("syncviews", False)

  #Build new window title from old 
  title = golly.getname().split('.')[0]
  title = '%s_Printer [%d] v0.9' % (title, pdy) 
  golly.new(title) 
 
  #Make sure we're using the standard Life generation rule
  golly.setrule("B3/S23")
 
  #Bitmap dimensions. Width MUST be of form 4m+1, for m >=1
  bmheight = len(bm)   
  bmwidth = len(bm[0])     
  mid = bmheight // 2 
   
  loopw = (bmwidth - 5) // 4 
  loopm = pdx * loopw 
 
  #Glider configurations list
  gg = gliders()

  #Glider loop ordering
  gnum = [0, 1] + loopw * [2, 1] + [3, 4, 6] + loopw * [5, 6]

  #Basic glider offsets. Sorry about all the magic numbers :) 
  gdeltas = [(24, 34), (34, 44), (22, 55), (44, 32), (33, 20), (8, 31), (20, 20)]
 
  #Actual glider offsets
  a = range(loopw + 1)
  t = [i * pdx for i in a for j in (0, 1)]
  tr = t[:]; tr.reverse()
  goff = (t + 2*[loopm] + tr)[:-1]
 
  #Eater horizontal displacement. Must be opposite parity to LMsx
  #bmwidth muliplier determines number of copies visible 'outside' printer.
  #4 complete columns are actually visible 'inside' printer.
  eX = (bmheight + 1) * LMdx - (copies * bmwidth - 4) * pdx 
  #Adjust parity
  eX = (LMsx + 1) % 2 + eX // 2 * 2

  def buildLM():
    ''' Build a complete LineMaker '''
    #Build glider loop. (jj, ii) are bitmap coords
    gloop = pattern()
    for j in xrange(bmwidth):
      jj = bmwidth - 1 - (j + LMsx * io) % bmwidth
     
      #Is there a pixel at this location?
      if not bm[ii][jj]:
        continue

      #Glider config number & offset     
      num, off = gnum[j], goff[j]
      x, y = gdeltas[num]
     
      #Add glider to the loop
      gloop += gg[num](x + off, y - off)   

    #Only put LineMaker if glider loop isn't empty
    if len(gloop) > 0:
      (LMFull + gloop).put(Lx, Ly, trans)
    #Add an eater to limit bitmap length. Could be in the
    #if block, but it looks neater with all eaters present. 
    eaters[(ii + it) % (1 + LMsx%2)].put(eX, Ly, trans)       

  #Assemble LineMaker
  Tdx, Tdy = 18 + loopm, 2 - loopm 
  LMFull = LMBase + LMTop.translate(Tdx, Tdy)
     
  transforms = (identity, flip_y)
 
  #Do upper LineMakers
  it = 0
  trans = transforms[it]
  for i in xrange(mid):
    ii = mid - (i + 1)
    io = mid + (i + 2)
    Lx, Ly = io * LMdx, ii * LMdy
    buildLM()

  #Do lower LineMakers     
  it = 1
  trans = transforms[it]
  for i in xrange(mid, bmheight):
    ii = i
    io = i + 2
    Lx, Ly = io * LMdx, ii * LMdy + 158
    buildLM()

  #Centre view over bitmap output area   
  golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
  golly.setmag(-2)    #Aliasing effects happen at smaller scales than -2 :(
  #golly.fit()
         
def main(): 
  #Vertical distance between pixels. Maybe get this from user.
  #minimum = 13 - LMsx%2; 18 seems to look best       
  pdy = 18

  #Generate & display a dot matrix printer from current selection
  dmprinter(pdy, copies=1)
 
  golly.setcursor(3)
  golly.setoption("hashing", True)
  golly.setbase(2)
  golly.setstep(6) 
 
main()


EDIT

Please see below for an updated version of this script using p30 technology.
Last edited by PM 2Ring on September 3rd, 2009, 7:01 am, edited 1 time in total.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » June 10th, 2009, 5:29 am

This script allows you to create a block of text in Life. The text is first converted into a bitmap, using a built-in font, then the bitmap pixels are tiled using the currently selected pattern, or a built-in pattern if there is no selection.

This script can be used in conjunction with the "bitmap printer" script I just posted, to create text tickers. Or it can be used to label large Life patterns with various annotations. Another fun thing to do is to make blocks of text from gliders or spaceships, and see what happens when words collide. :)

The latest version asks for a block size if no pattern is currently selected. A size of 1 (the default) uses a single live cell (which is useful for making text for my printer script), a size of 2 uses a single live cell in a 2x2 box (which is useful for making vanishing text), larger sizes use a block in a size x size box.

textprint3.py
#Golly Python script
#Written by PM 2Ring 2009.4.5. Updated 2009.6.20, 2009.6.28

''' Make a Life pattern from text.

The text string can be entered on the status line, or hit Return to open
a GTK file dialog to read the text from a file.

The text is first converted into a bitmap, then the bitmap pixels are
tiled using the currently selected pattern. If there is no selection,
a built-in pattern is used.

The script asks for the desired bitmap width on the status line.
Any text lines that are too long will be wrapped at word breaks. Large texts are rendered progressively to the screen.

Uses font data from http://pentacom.jp/soft/ex/font/
This is NOT a fixed width font.
'''

import zlib, base64, golly
from glife import *

import pygtk
pygtk.require('2.0')
import gtk

#-----------------------------------------------------------------
#Text stuff

def hexbin(bitchars='01'):
    ''' Create a dict for hex string to bit string conversion '''
    hb = {}
    b = ['']*4
    for i in xrange(16):
        for j in xrange(4):
            b[3-j] = bitchars[i >> j & 1]
        hb['%x' % i] = ''.join(b)
    return hb

def fontparse(bitchars):
    ''' Parse font data & return glyphs in a dict.
        bitchars are used to encode bits '''
    #Decompress font data into list s, splitting on commas
    s = zlib.decompress(base64.decodestring('''
    eNqdVm2WgyAMvND+QEQNx7EW7n+EhQo0gSS463uptE4n5IMxxjy7fvIHMGbQvQHPYlPGfG3JItx3
    vG5Amz6OzAg3a15buL9/1o3RGePc/cAlW9KTJVFZ+wU113syX2wv7HkdLiHqaTBOSJHj8ugYG/II
    QFMCoOTRFstga2jEBMhd2P0AjLMSctcQzFKs7s11KWvAo0Ta28El/GLyGHpgZcx+VuQ2wgRoJNc5
    kLU0rC9MNUACjCjCgLIg7hHqnyTXsdBxuTRSHg+NkTxE7DtXa1DqTIBOaTGSR4seuvJ9kdqsliyC
    IlKYxUqua9Q4j8YwSvEBpz6D9MQnATqTvZK5oOQxds07NkUFdmuxKZqxTSGcmch1T2XCvTkcBQkI
    2h69dlzJvtT0IAWznbLRynRSq7r25aV0lbs3jGsQbHCd//kuLyHQgqk/XgXsJeBj2Ru6R6xMBjlq
    n/N9TRjF9OBaH5o+SiW0WlPA0+6ptmmMJ7I6W7DAyrKZSQm3WTC9NIvvmUtos0sauRxSNWvou6Yx
    cjMFYXR/nc24K4KguI8ZDzR6+dKnbNShAwaNEefxkIaPwbXKCF8ZJHtcUYKjMsjpUaPhU43ac3MP
    V/PARW3KpLJ0xrquGhkk16AYm8e3oSfxBAE4jbqvjKgUEmNAeyXAnalMTRMBvpijq/bjLk17Uves
    EqN/2rhYrDZpnKlAVR8xsNfIx3tccJthCcEaGTnGvVOxut7/3Y89S6+RrTIn0Am0v34BSmb90A==
    ''')).split(',')

    #Remove trailing \n
    s[-1] = s[-1][:-1]

    #Build hex string to binary string conversion dict
    hb = hexbin(bitchars)

    #Convert hex strings to bit strings
    b = [''.join([hb[j] for j in i]) for i in s]

    #Extract font glyphs
    glyphs = {}
    for i,v in enumerate(b):
        c = chr(32+i)
        #Split into 10 rows of width 16
        rows = [v[j:j+16] for j in range(0, 160, 16)]
        if i:
            #Find glyph width & remove extra space
            rw = max([r.rfind(bitchars[1]) for r in rows])
            rw = min(2+rw, 16)
        else:
            #For the blank space
            rw = 2
        glyphs[c] = [r[:rw] for r in rows]
    return glyphs

class linebuffer:
    def __init__(self, mwidth, height, blank):
        self.mwidth = mwidth
        self.hrange = xrange(height)
        self.blank = blank
        self.clear()

    def clear(self):
        self.data = [[] for i in self.hrange]
        self.width = 0

    def append(self, glist):
        ''' Append glyphs in glist to buffer '''
        for g in glist:
            for i in self.hrange:
                self.data[i] += g[i]
            self.width += len(g[0])

    def dump(self, page):
        ''' Copy buffer to string list page & reset '''
        if self.width == 0:
            return
        pad = [(self.mwidth - self.width) * self.blank]
        for i in self.hrange:
            page += [''.join(self.data[i] + pad)]
        self.clear()

def settext(text, width, bitchars):
    ''' Typeset text string from glyphs composed of bitchars,
    with word wrapping to given line width. Return a list of lines '''
    glyphs = fontparse(bitchars)
    blank = glyphs[' ']
    buff = linebuffer(width, 10, bitchars[0])
    page = []
    for line in text.splitlines():
        for word in line.split(' '):
            glist = [glyphs.get(c, blank) for c in word] + [blank]
            gw = sum([len(g[0]) for g in glist])
            if gw + buff.width > width:
                buff.dump(page)
            buff.append(glist)
        buff.dump(page)
    return page

#-----------------------------------------------------------------
#Golly stuff

def getfilename(title='Open...', folder=''):
    ''' Get a file name from folder '''
    dialog = gtk.FileChooserDialog(title, None,
        gtk.FILE_CHOOSER_ACTION_OPEN,
        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
        gtk.STOCK_OPEN, gtk.RESPONSE_OK))
    dialog.set_current_folder(folder)     #else it opens in script folder
    #dialog.set_filename(folder + fname)  #if used, can omit prev line

    filters = (('All files', '*'), ('Text files', '*.txt'))
    for name, pattern in filters:
        filter = gtk.FileFilter()
        filter.set_name(name)
        filter.add_pattern(pattern)
        dialog.add_filter(filter)

    fname = dialog.run() == gtk.RESPONSE_OK and dialog.get_filename()
    dialog.destroy()
    return fname

def tile0(cells, pat, px, py):
    ''' Convert live cells in cell-list cells into copies of the pattern pat, size px*px. '''
    golly.addlayer()

    cellsxy = [(cells[i], cells[i+1]) for i in xrange(0, len(cells), 2)]
    all = pattern()
    for x, y in cellsxy:
        all += pat(x*px, y*py)
    all.display()

def tile(cells, pat, px, py):
    ''' Convert live cells in cell-list cells into copies of the pattern pat, size px*px.
    Regularly update the display as tiles are placed.
    '''
    golly.addlayer()

    all = pattern()
    for i in xrange(0, len(cells), 2):
        x, y = cells[i:i+2]
        all += pat(x*px, y*py)
        if i % 5002 == 5000:
            all.put()
            all = pattern()
            golly.fit()
            golly.update()
    all.put()
    golly.fit()

def main():
    #Get currently selected pattern, to use as a tile to render font glyph pixels.
    #If no selection, use built-in patterns
    selrect = golly.getselrect()
    if len(selrect) > 0:
        #Make top left corner of selection the origin of the tiling pattern
        ox, oy, x, y = selrect
        pat = pattern(golly.getcells(selrect), -ox, -oy)
    else:
        numstr = getstring('No selection. Enter block size default=1) :')
        bsize = 1
        if numstr != '':
            if not validint(numstr):
                golly.exit('Bad number: %s' % numstr)
            bsize = int(numstr)
        if bsize<1:
            golly.exit('Bad size: %s' % numstr)
        elif bsize<3:
            #Single cell   
            pat = pattern([0,0])   
        else:
            #Block
            pat = pattern('2o$2o!')               
        x = y = bsize           
       
    width = 400                        #Default text bitmap line width
    minwidth = 32
    prompt = 'Enter text bitmap line width (minimum=%d, default=%d) :' % (minwidth, width)
    numstr = getstring(prompt)
    if numstr != '':
        if not validint(numstr):
            golly.exit('Bad number: %s' % numstr)
        width = int(numstr)
        if width < minwidth:
            width = minwidth

    text = getstring('Enter text string or hit Return to open a text file:')
    if text == '':
        fname = getfilename('Open a text file', folder=golly.appdir())
        if not fname:
            golly.exit('No text file selected, aborting.')
        #print 'file', fname
        f = open(fname, 'r')
        text = f.read()
        f.close()

    golly.show('Parsing text...')
    page = settext(text, width, bitchars='.*')
    #for row in page: print row

    textcells = list(pattern('\n'.join(page)))
    #golly.putcells(textcells)

    golly.show('Converting text...')
    tile(textcells, pat, x, y)
    golly.show('')

if __name__ == '__main__':
    main()


Edit. A minor update to this script. It now uses a single live cell in a 2x2 box if no pattern is currently selected. This is handy for making text that dies in one step.

Edit#2. Oops! The previous script had a bug/typo I just noticed today. Sorry about that. :oops:
Last edited by PM 2Ring on June 28th, 2009, 9:02 am, edited 2 times in total.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » June 12th, 2009, 10:00 am

This script creates p30 memory loops using the duplicator-inverter Queen Bee reaction found by Dietrich Leithner. The loops can be square or rectangular, but if you use rectangles, both dimensions must have the same parity, that is, both must be even or both odd. It's not possible to have loops of mixed parity using this method, as the glider stream gets out of phase.

# Golly python script.
# Written by PM 2Ring, April 2009

''' Create a p30 memory loop. '''

from glife import *
import golly
 
class ArgError(Exception): pass 

def getargs():
  ''' Get input data '''
 
  prompt = 'Enter loop dimension(s)'
  usage = 'Create a p30 memory loop.\n' + prompt + ''', separated by spaces.
If only one value is given, a square loop will be created.
Dimensions must have the same parity, that is, both must be even or both odd.'''
 
  #Loop until we get correct args, or are aborted by ESC   
  while True:
    try:
      args = getstring(prompt + ' :').split()
      if args == []:
        raise ArgError
     
      #Check that args are valid integers
      for s in args: 
        if not validint(s):
          raise ArgError, '[%s] is not a valid integer.' % s     

      #Convert arguments to integer
      args = [int(s) for s in args]           
      for i in args:
        if i<=0:
          raise ArgError, 'Data must be > 0, not %d!' % i
     
      if len(args) < 2:
        args += args
      else:
        #Check that parity matches
        if args[0]%2 != args[1]%2:
          ps = ('even', 'odd')
          t = (args[0], ps[args[0]%2], args[1], ps[args[1]%2])
          raise ArgError, 'Parity mismatch. %d is %s but %d is %s!' % t
       
    except ArgError, s:
      g.warn('%s\n\n%s' % (s, usage)) 
    else:   
      break 
  return args 
     
def memloop(x, y):
  ''' Create a memory loop. '''
 
  #Queen bee reflector / duplicator 
  qb = pattern('''34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
  $2o11bo2bo7b3o7b2o$13bobo9b2o7bo$12bobo17bobo$12bo9bo9b2o$23b2o$22b2o7$24b2o$24b2o!''')
  qb2 = qb(0, 0, rcw)
  qb3 = qb(0, 0, flip)
  qb4 = qb(0, 0, rccw)
 
  dd = 15
  dx = x // 2 * dd
  dy = y // 2 * dd
 
  if x%2==0:
    qb2 = qb2[2]
    qb4 = qb4[2]
    dx -= 8
    dy -= 7
     
  qb.put()
  qb2.put(48 + dx, 0 + dx)
  qb3.put(48 + dx - dy, 48 + dx + dy)
  qb4.put(0 - dy, 48 + dy)
   
def main():
  #Get input data
  x, y = getargs()
  #x, y = 4, 6

  #Make memloop in a new layer
  golly.duplicate()
  golly.setoption("syncviews", False)

  title = 'MemLoop %d x %d' % (x, y) 
  golly.new(title)
  memloop(x, y)
     
  golly.fit()
     
if __name__ == '__main__':
  main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby Nathaniel » June 17th, 2009, 7:37 pm

This script fills a rectangle randomly with a user-specified density. It was created to rectify the problem that is described in its comment header (Problem #1 mentioned in this post). This problem is only present in Golly 2.0 and earlier; it has been fixed as of Golly 2.1.

# A randomized rectangle filler for use with Golly.
# Author: Nathaniel Johnston (nathaniel@nathanieljohnston.com), June 2009.
#
# Even though Golly comes built-in with the golly.randfill() function, there
# does not seem to be a way to seed that function, so you get the same sequence
# of random rectangles every time you reload Golly. This script eliminates that
# problem.


import golly as g
import random
random.seed()

# --------------------------------------------------------------------

def randfill(rectcoords, amt):
   for i in range(rectcoords[0],rectcoords[0]+rectcoords[2]):
      for j in range(rectcoords[1],rectcoords[1]+rectcoords[3]):
         if(100*random.random()<amt):
            g.setcell(i, j, 1)
         else:
            g.setcell(i, j, 0)

# --------------------------------------------------------------------

if len(g.getselrect()) == 0: g.exit("No selection.")
s = g.getstring("Enter the fill percentage (0.0 - 100.0):","", "Random filler")
try:
   amt = float(s)
   if amt < 0 or amt > 100:
      g.exit('Bad number: %s' % s)
except ValueError:
   g.exit('Bad number: %s' % s)
 
randfill(g.getselrect(),amt)
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby PM 2Ring » June 20th, 2009, 10:17 am

Text string to Snakial converter.

A simple script to make it easier to use the Snakial font in Golly. The basic Snakial font only has glyphs for the decimal digits and the hyphen dash (minus sign). This script adds a space character, and a block for the decimal point / period. All other characters are rendered as dashes.

The top left corner of the current selection is used as the baseline of the text. If no text string is entered, a sample string of all Snakial glyphs is rendered.

SnakialText.py
# Golly python script. Written by PM 2Ring, June 2009

''' Render a numeric text string using the Snakial font.

Note this font only contains glyphs for the digits, hyphen, dot and space.
All other characters will be rendered as a hyphen.
'''

from glife.text import make_text, __sfont

#Add some characters to the font
__sfont['.'] = pattern ("2o$2o!", 0, -2)
__sfont['.'].width = 6
__sfont[' '] = pattern ()
__sfont[' '].width = 6

def put_text(text, x=0, y=0):
    ''' Render text in Snakial font at (x, y) '''
    make_text(text).put(x, y)

def main():
    #The top left corner of the selection is used as the baseline of the text   
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection.")
   
    text = getstring('Enter one or more numeric text strings to render:')
    if not text:
        text = '- 0 1 2 3 4 5 6 7 8 9 .'
        #golly.exit('No text given, aborting.')
       
    put_text(text, *selrect[:2])
     
if __name__ == '__main__':
  main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby Nathaniel » June 23rd, 2009, 5:29 pm

Since it's not built into Golly, here's a script that computes the current pattern's RLE encoding:

# RLE computation script for use with Golly.
# Author: Nathaniel Johnston (nathaniel@nathanieljohnston.com), June 2009.

# Shows the RLE encoding of the current pattern


import golly as g

# --------------------------------------------------------------------

def chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i+n]

# --------------------------------------------------------------------

def giveRLE(rl_list):
   rle_res = ""
   rle_len = 1
   rl_y = rl_list[0][1] - 1
   rl_x = 0
   for rl_i in rl_list:
      if rl_i[1] == rl_y:
         if rl_i[0] == rl_x + 1:
            rle_len += 1
         else:
            if rle_len == 1: rle_strA = ""
            else: rle_strA = str (rle_len)
            if rl_i[0] - rl_x - 1 == 1: rle_strB = ""
            else: rle_strB = str (rl_i[0] - rl_x - 1)

            rle_res = rle_res + rle_strA + "o" + rle_strB + "b"
            rle_len = 1
      else:
         if rle_len == 1: rle_strA = ""
         else: rle_strA = str (rle_len)
         if rl_i[1] - rl_y == 1: rle_strB = ""
         else: rle_strB = str (rl_i[1] - rl_y)
         if rl_i[0] == 1: rle_strC = "b"
         elif rl_i[0] == 0: rle_strC = ""
         else: rle_strC = str (rl_i[0]) + "b"
         
         rle_res = rle_res + rle_strA + "o" + rle_strB + "$" + rle_strC
         rle_len = 1

      rl_x = rl_i[0]
      rl_y = rl_i[1]
   
   if rle_len == 1: rle_strA = ""
   else: rle_strA = str (rle_len)
   rle_res = rle_res[2:] + rle_strA + "o"
   
   return rle_res
     
# --------------------------------------------------------------------

clist = list (chunks (g.getcells (g.getrect()), 2))
mcc = min(clist)
clist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in clist]
     
g.show (giveRLE (clist))
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby Nathaniel » June 26th, 2009, 10:05 am

I recently had a need for a script that computes the RLE of a pattern in all of its possible rotation/reflection orientations, so here it is. It returns the RLE encodings (up to 8 of them), one per line, in your clipboard.

This script requires Golly 2.1 or later to run.

getrlerotations.py
# RLE computation script for use with Golly.
# Author: Nathaniel Johnston (nathaniel@nathanieljohnston.com), June 2009.

# Gives the RLE encoding of the pattern in all of its possible orientations (up to 8).
# The first RLE string returned is the orientation that the pattern was given in.
# Data is returned to the clipboard.

import golly as g

# --------------------------------------------------------------------

def chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i+n]

# --------------------------------------------------------------------

def giveRLE(rl_list):
   rle_res = ""
   rle_len = 1
   rl_list.sort(cmp = lambda x,y: (x[0]-y[0])+500*(x[1]-y[1]))
   rl_y = rl_list[0][1] - 1
   rl_x = 0
   for rl_i in rl_list:
      if rl_i[1] == rl_y:
         if rl_i[0] == rl_x + 1:
            rle_len += 1
         else:
            if rle_len == 1: rle_strA = ""
            else: rle_strA = str (rle_len)
            if rl_i[0] - rl_x - 1 == 1: rle_strB = ""
            else: rle_strB = str (rl_i[0] - rl_x - 1)

            rle_res = rle_res + rle_strA + "o" + rle_strB + "b"
            rle_len = 1
      else:
         if rle_len == 1: rle_strA = ""
         else: rle_strA = str (rle_len)
         if rl_i[1] - rl_y == 1: rle_strB = ""
         else: rle_strB = str (rl_i[1] - rl_y)
         if rl_i[0] == 1: rle_strC = "b"
         elif rl_i[0] == 0: rle_strC = ""
         else: rle_strC = str (rl_i[0]) + "b"
         
         rle_res = rle_res + rle_strA + "o" + rle_strB + "$" + rle_strC
         rle_len = 1

      rl_x = rl_i[0]
      rl_y = rl_i[1]
   
   if rle_len == 1: rle_strA = ""
   else: rle_strA = str (rle_len)
   rle_res = rle_res[2:] + rle_strA + "o"
   
   return rle_res
     
# --------------------------------------------------------------------

clist = g.getcells(g.getrect())
reslist = []
resstr = ""
axx = [1,1,-1,-1,0,0,0,0]
ayy = [1,-1,1,-1,0,0,0,0]
axy = [0,0,0,0,1,1,-1,-1]
ayx = [0,0,0,0,1,-1,1,-1]

for i in range(0,8):
   dlist = list (chunks (g.transform(clist,0,0,axx[i],axy[i],ayx[i],ayy[i]), 2))
   mcc = min(dlist)
   dlist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in dlist]
   curRLE = giveRLE(dlist)
   if (i == 0):
      firstRLE = curRLE   
   reslist.append(curRLE)

reslist = list(set(reslist))

for i in range(0,len(reslist)):
   if(reslist[i] == firstRLE):
      resstr = reslist[i] + "\n" + resstr
   else:
      resstr = resstr + reslist[i] + "\n"
   
g.setclipstr(resstr)
g.show("RLE strings added to clipboard (" + str(len(reslist)) + " string(s) total).")
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby Nathaniel » June 27th, 2009, 9:50 am

This script tells you all rules that the current pattern (assumed to be a still life, oscillator, or spaceship) works in. It asks you for its period and then finds all rules under which all of its phases match the phases that are produced by Golly's current rule. Thus, for the script to work correctly, you must have Golly set to a rule that the pattern works under (so if you want to know what rules a glider works in, before running this script set Golly to Life, HighLife or another rule that the glider works in, but not a rule like Seeds).

Also, the code is pretty slow and a bit of a mess, but it works for small patterns, which is all I really need it for.

getallrules.py
# Rule computation script for use with Golly.
# Author: Nathaniel Johnston (nathaniel@nathanieljohnston.com), June 2009.

# Gives the maximal family of rules that a still life, oscillator, or spaceship
# works under. Must be called while the rule is set of one such family
# For example, to find out what rules a glider works in, first set the rule
# to Life or HighLife, not Seeds.

from glife import getstring, validint
import golly as g

# --------------------------------------------------------------------

def chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i+n]

# --------------------------------------------------------------------

def giveRLE(rl_list):
   rle_res = ""
   rle_len = 1
   rl_list.sort(cmp = lambda x,y: (x[0]-y[0])+500*(x[1]-y[1]))
   rl_y = rl_list[0][1] - 1
   rl_x = 0
   for rl_i in rl_list:
      if rl_i[1] == rl_y:
         if rl_i[0] == rl_x + 1:
            rle_len += 1
         else:
            if rle_len == 1: rle_strA = ""
            else: rle_strA = str (rle_len)
            if rl_i[0] - rl_x - 1 == 1: rle_strB = ""
            else: rle_strB = str (rl_i[0] - rl_x - 1)

            rle_res = rle_res + rle_strA + "o" + rle_strB + "b"
            rle_len = 1
      else:
         if rle_len == 1: rle_strA = ""
         else: rle_strA = str (rle_len)
         if rl_i[1] - rl_y == 1: rle_strB = ""
         else: rle_strB = str (rl_i[1] - rl_y)
         if rl_i[0] == 1: rle_strC = "b"
         elif rl_i[0] == 0: rle_strC = ""
         else: rle_strC = str (rl_i[0]) + "b"
         
         rle_res = rle_res + rle_strA + "o" + rle_strB + "$" + rle_strC
         rle_len = 1

      rl_x = rl_i[0]
      rl_y = rl_i[1]
   
   if rle_len == 1: rle_strA = ""
   else: rle_strA = str (rle_len)
   rle_res = rle_res[2:] + rle_strA + "o"
   
   return rle_res
     
# --------------------------------------------------------------------

rule = g.getrule().upper()

if not rule[0]=="B" or not "/S" in rule:
   g.exit("Please set Golly to a Life-like rule.")
   
if g.empty(): g.exit("The pattern is empty.")
s = g.getstring("Enter the period:","", "Rules calculator")

if not validint(s):
  g.exit('Bad number: %s' % s)
numsteps = int(s)
if numsteps < 1:
  g.exit('Period must be at least 1.')
   
g.select(g.getrect())
g.copy()
s = int(s)

for i in range(0,s):
   g.run(1)
   clist[i] = list (chunks (g.getcells(g.getrect()), 2))
   mcc = min(clist[i])
   clist[i] = [[x[0]-mcc[0],x[1]-mcc[1]] for x in clist[i]]

g.show('Processing...')

ruleArr = rule.split("/")
ruleArr[0] = ruleArr[0][1:]
ruleArr[1] = ruleArr[1][1:]

b_need = []
b_OK = []
s_need = []
s_OK = []

for i in ruleArr[0]:
   b_need.append(i)
   b_OK.append(i)
for i in ruleArr[1]:
   s_need.append(i)
   s_OK.append(i)

for i in range(0,9):
   if not str(i) in b_OK:
      b_OK.append(str(i))
      g.setrule("B" + "".join(b_OK) + "/S" + ruleArr[1])
      for j in range(0,s):
         g.run(1)
         try:
            dlist = list (chunks (g.getcells(g.getrect()), 2))
            mcc = min(dlist)
            dlist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in dlist]
            if not(clist[j] == dlist):
               b_OK.remove(str(i))
               break
         except:
            b_OK.remove(str(i))
            break
      g.new("")
      g.paste(0,0,"or")
      g.select(g.getrect())     
     
   if not str(i) in s_OK:
      s_OK.append(str(i))
      g.setrule("B" + ruleArr[0] + "/S" + "".join(s_OK))
      for j in range(0,s):
         g.run(1)
         try:
            dlist = list (chunks (g.getcells(g.getrect()), 2))
            mcc = min(dlist)
            dlist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in dlist]
            if not(clist[j] == dlist):
               s_OK.remove(str(i))
               break
         except:
            s_OK.remove(str(i))
            break
      g.new("")
      g.paste(0,0,"or")
      g.select(g.getrect())     

   if str(i) in b_need:
      b_need.remove(str(i))
      g.setrule("B" + "".join(b_need) + "/S" + ruleArr[1])
      for j in range(0,s):
         g.run(1)
         try:
            dlist = list (chunks (g.getcells(g.getrect()), 2))
            mcc = min(dlist)
            dlist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in dlist]
            if not(clist[j] == dlist):
               b_need.append(str(i))
               break
         except:
            b_need.append(str(i))
            break
      g.new("")
      g.paste(0,0,"or")
      g.select(g.getrect())     

   if str(i) in s_need:
      s_need.remove(str(i))
      g.setrule("B" + ruleArr[0] + "/S" + "".join(s_need))
      for j in range(0,s):
         g.run(1)
         try:
            dlist = list (chunks (g.getcells(g.getrect()), 2))
            mcc = min(dlist)
            dlist = [[x[0]-mcc[0],x[1]-mcc[1]] for x in dlist]
            if not(clist[j] == dlist):
               s_need.append(str(i))
               break
         except:
            s_need.append(str(i))
            break
      g.new("")
      g.paste(0,0,"or")
      g.select(g.getrect())     

g.setrule(rule)
ruleres = "B" + "".join(sorted(b_need)) + "/S" + "".join(sorted(s_need)) + " - B" + "".join(sorted(b_OK)) + "/S" + "".join(sorted(s_OK))
g.show(ruleres)
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby PM 2Ring » June 28th, 2009, 9:12 am

This script saves a sequence of generations in pbm ASCII format. Thus it can be used to create animations of Life patterns. It's just a minor variation of the SavePBM script I posted previously. Some display programs don't handle this format properly, but it can be converted to other formats like png or GIF, eg using a NetPBM program or ImageMagick's convert.

I find the pbm ASCII format useful, as it can be read & edited by normal text processing tools and it's easy to convert to other ASCII formats, like the 'o.' format used in the Life Lexicon, or '*.' format. Note that Goly can also load such formats.

# Save selected Golly pattern as a plain PBM file

from glife import *
import golly

import pygtk
pygtk.require('2.0')
import gtk

class bitmap:
  ''' Basic pixel array, with plain PBM output '''
  def __init__(self, x, y):
    self.x, self.y = x, y

    #Pixel buffer. 0=white, 1=black.
    row = x * ['0']
    self.pix = [row[:] for i in xrange(y)]

  #Put pixel in buffer.
  def put(self, x, y, c=1):
    self.pix[y][x] = '01'[c]

  def savepbm(self, fname):
    f = open(fname, 'w')
    f.write('P1\n%d %d\n' % (self.x, self.y))
    #Technically, each row should be no more than 70 chars,
    # but the NetPBM progs are usually lenient...
    bmax = 70
    for row in self.pix:
      #Cut row into blocks of length bmax
      blocks = [''.join(row[i:i+bmax]) for i in xrange(0, self.x, bmax)]
      for i in blocks:
        f.write(i + '\n')
    f.close()

def getfilename(title='Save...', folder=''):
  ''' Get a file name from folder '''     
  dialog = gtk.FileChooserDialog(title, None,
    gtk.FILE_CHOOSER_ACTION_SAVE,
    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
    gtk.STOCK_SAVE, gtk.RESPONSE_OK))
  dialog.set_default_response(gtk.RESPONSE_OK)
 
  dialog.set_current_folder(folder)     #else it opens in script folder
  #dialog.set_filename(folder + fname)  #if used, can omit prev line
 
  filters = (('PBM files', '*.pbm'), ('All files', '*'))
  for name, pattern in filters:
    filter = gtk.FileFilter()
    filter.set_name(name)
    filter.add_pattern(pattern)   
    dialog.add_filter(filter) 
 
  fname = dialog.run() == gtk.RESPONSE_OK and dialog.get_filename()   
  dialog.destroy()
  return fname 
 
def save(fname):
  selrect = golly.getselrect()
  if len(selrect) == 0: golly.exit("There is no selection.")
  golly.show('Saving contents of %s to %s' % (str(selrect), fname))
 
  #Initialize bitmap pixel buffer
  pix = bitmap(*tuple(selrect[2:]))   

  #Make top left corner of selection the top left corner of the saved image   
  x, y = selrect[:2] 
  cells = golly.getcells(selrect) 
  cellsxy = [(cells[i]-x, cells[i+1]-y) for i in xrange(0, len(cells), 2)]
  for t in cellsxy:
    pix.put(*t)
   
  pix.savepbm(fname) 
     
def main():
  folder = golly.appdir() + 'Pictures' 
  fname = getfilename("Select PBM base name for frames", folder)
  if not fname:
    golly.exit('No filename selected, aborting.')
   
  namefmt = fname.replace('.pbm', '%03d.pbm')

  while True: 
    s = getstring('Number of steps :')
    if not validint(s):
      golly.warn('[%s] is not a valid integer.' % s)
    else:
      break
 
  for i in xrange(int(s)):
    fname = namefmt % i 
    #golly.show('Frame #%d: %s' % (i, fname))
    save(fname)
    #Evolve selection by 1 step
    golly.advance(0, 1)   
   
  golly.show('Finished')
 
if __name__ == '__main__':
  main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby Nathaniel » July 1st, 2009, 9:25 am

Here's a script for running a toroidal universe in Golly (as opposed to the infinite planar universe that it uses by default). The toroidal region it uses is whatever the current selection is. Note that it's not particularly fast, as it updates the display after each generation (just remove the display lines of code and it speeds up considerably).

torus.py
# A script that emulates Life on a toroidal universe, rather than the infinite planar universe that is the default in Golly
# Author: Nathaniel Johnston (nathaniel@nathanieljohnston.com), June 2009.

# The torus used is whatever the current selection in Golly is
# The user may enter the number of generations that they would like the pattern to evolve for

from glife import getstring, validint
import golly as g

# --------------------------------------------------------------------

def run_torus(gens):
   for i in range(0,gens):
      top_cells = g.getcells([cur_sel[0],cur_sel[1],cur_sel[2],1])
      bot_cells = g.getcells([cur_sel[0],cur_sel[1]+cur_sel[3]-1,cur_sel[2],1])
      lef_cells = g.getcells([cur_sel[0],cur_sel[1],1,cur_sel[3]])
      rig_cells = g.getcells([cur_sel[0]+cur_sel[2]-1,cur_sel[1],1,cur_sel[3]])
      g.putcells(top_cells, 0, cur_sel[3])
      g.putcells(bot_cells, 0, -cur_sel[3])
      g.putcells(lef_cells, cur_sel[2], 0)
      g.putcells(rig_cells, -cur_sel[2], 0)
      g.setcell(cur_sel[0]-1, cur_sel[1]-1, g.getcell(cur_sel[0]+cur_sel[2]-1,cur_sel[1]+cur_sel[3]-1))
      g.setcell(cur_sel[0]+cur_sel[2], cur_sel[1]-1, g.getcell(cur_sel[0],cur_sel[1]+cur_sel[3]-1))
      g.setcell(cur_sel[0]-1, cur_sel[1]+cur_sel[3], g.getcell(cur_sel[0]+cur_sel[2]-1,cur_sel[1]))
      g.setcell(cur_sel[0]+cur_sel[2], cur_sel[1]+cur_sel[3], g.getcell(cur_sel[0],cur_sel[1]))
      g.run(1)
      g.clear(1)
      g.dokey(g.getkey())            # allow keyboard interaction
      g.update()

cur_sel = g.getselrect()
if len(cur_sel) == 0: g.exit("Please make a selection.")

s = g.getstring("Enter the number of generations to run for:","", "Toroidal evolution")

if not validint(s):
  g.exit('Bad number: %s' % s)

g.show('Processing...')
run_torus(int(s))
User avatar
Nathaniel
Site Admin
 
Posts: 407
Joined: December 10th, 2008, 3:48 pm
Location: New Brunswick, Canada

Re: Golly scripts

Postby PM 2Ring » July 6th, 2009, 6:05 am

That toroidal script looks intriguing, Nathaniel; I'll have to check it out. I realize it won't be very fast, but it's nice to be able to do such things in Golly, since it's my favourite Life program for the PC. (Although from a user interface POV, Golly is still not quite as good as my favourite Amiga Life program (which also has script control, using ARexx), but the poor old Amiga is a bit slow these days for playing with Life patterns :)).
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » July 6th, 2009, 6:07 am

A simple script to find the minimum & maximum population counts of a pattern.

# Golly python script.
# Written by PM 2Ring, July 2009

''' Evolve current pattern step by step, tracking population '''

from glife import *
import golly
 
def popcount():
    prompt = 'Enter number of generations :'   
    data = getstring(prompt).split()
    if not data:
        golly.exit()         
   
    numstr = data[0]
    if not validint(numstr):
        golly.exit('Bad number: %s' % numstr)
    num = int(numstr)     
   
    pop = int(golly.getpop())
    minpop = maxpop = pop
    #print 'Gen: pop [min - max]'
    for i in xrange(num+1):
        #s = '%d: %d [%d - %d]' % (i, pop, minpop, maxpop)
        #print s
        #golly.show(s)   
        golly.run(1)
        pop = int(golly.getpop())
        if pop<minpop:
            minpop = pop
        elif pop>maxpop:
            maxpop = pop
   
    golly.show('min=%d, max=%d' % (minpop, maxpop))
     
def main():
    popcount() 
   
if __name__ == '__main__':
    main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » July 6th, 2009, 6:19 am

Herschel track builder, using David Buckinghams 8 original Herschel conduits and Paul Callahan's variable sized Herschel transceiver. Please read the notes at the start of the script for more info.

I will improve this script, once I have more basic conduits to add to DJB's set of 8. I may also add logic to detect broken tracks caused when conduits overlap or otherwise interfere with each other.

#Golly Python script

''' Herschel track builder.

    Derived from track.py, which is based on track.py from PLife.
    Written by PM 2Ring 2009.7.3
'''

import golly
from glife import *
from glife.herschel import *

notes = '''
Herschel conduits
-----------------

The Herschel is a commonly-occuring heptomino. A B-heptomino becomes a
Herschel (plus a block) in twenty generations. The B-heptomino (and
hence the Herschel) is used in many p46 patterns; most notably Bill
Gosper's "new gun", the second glider gun ever found, uses 4 of these.

Various combinations of still life can be made to assist the forward
propagation of a Herschel. Such patterns are called Herschel conduits.
With the complete set, a glider gun of any period greater than 61 can
be constructed.

The glife.herschel module contains definitions for all 8 of David J.
Buckingham's original Herschel conduits. It also provides the "ghost"
Herschel: a vanishing pattern which is used to indicate Herschel input
and output locations.
   
herschel
***
*
***

herschel_ghost
***
.
*.*
   
The table below lists all the conduits in herschel.py. It combines data
from that file and from the table given in track.py. The number at the
end of the pattern name is the number of steps it takes for a Herschel
to travel through the conduit. The second column lists the transformation
that the conduit applies to the Herschel, starting from the orientation
shown above. The third column lists the conduit repeat time: the minimum
number of generations that is possible between the arrival of one Herschel
and the arrival of the next. The fourth column lists the number of gliders
the conduit produces and miscellaneous remarks.

Name  Transformation      Tightest Compression; Notes
----  --------------      ---------------------------
hc64  ( -9,  -9, rccw)    153 (also 73, 74, 77); 1 glider, aimed inward.
hc77  ( 10, -25, flip_x)   61; 1 glider (natural b-heptomino glider).
hc112 ( 35, -12, rcw)      61; 1 glider, aimed outward.
hc117 (  6, -40, identity) 63; 1 glider. Can be perturbed by single-spark oscs.
hc119 (-12, -20, flip_x)  231; 3 gliders.
hc156 ( 43, -17, rcw)      62; 2 gliders: Normal B-hepto glider + one more.
hc158 (  7, -27, flip_x)  176 (also 101, 102, 109, 111, 159); 1 glider.
hc190 (-16, -22, rccw)    107; 1 glider, aimed inward.   

Herschel transceiver
--------------------

A Herschel transceiver is an adjustable Herschel conduit made up of a
Herschel transmitter and a Herschel receiver. The intermediate stage
consists of two gliders on parallel tracks, so the transmitter and
receiver can be separated by any required distance.

Note that these names are glider-oriented: the Herschel transmitter
converts a Herschel into a pair of gliders and the Herschel receiver
converts the glider pair back into a Herschel.
   
The herschel_transceiver function below uses a stable transmitter and
receiver pair found by Paul Callahan in 1996 & 1997. The receiver uses
the R-to-Herschel converter found by David Buckingham in 1972.

The step value of the Herschel transceiver is 313 + 4n. It transforms
the Herschel by (-(n + 5), n + 65, flip_y), where n is an integer >= 0.
Compression for the transceiver is 117; it produces no external gliders.
'''

#Herschel transceiver parts.
#Transmitter.
hc_tx = pattern('''x = 18, y = 19, rule = B3/S23
9b2o$9bo$3b2o5bo$2bobo4b2o$2bo$b2obo$o2b2o$2o8$14b2o$14bobo$16bo$
16b2o''', -3, -13)

#Receiver
hc_rx = pattern('''x = 32, y = 46, rule = B3/S23
10b2o$10b2o$6bo$6b3o$9bo$8b2o11bo$20bobo7b2o$20bobo7b2o$21bo5$18b2o$
18b2o$4b2o$4b2o$2o$2o14$24b2o$23bobo$24bo7$26b2obo$26bob2o2$19b2o$
19b2o''', -22, 2)
hc_rx.transform = (-5, 65, flip_y)

def herschel_transceiver(steps):
    ''' Build a Herschel transceiver.
   
    steps is the number of generations that it takes for the
    Herschel to appear at the far end of the conduit.
       
    steps should be congruent to 1 mod 4, and >= 313
    It will be automatically rounded up, if necessary.
    Small conduits may not be easy to interconnect.
    In order to chain copies of this conduit together, the
    minimum gen is 393.   
    '''

    #Adjust steps to a "legal" value
    asteps = max(313, steps)
    m = (asteps - 1) % 4
    if m > 0:
        asteps += 4 - m
       
    if asteps != steps:
        w = 'Warning: Bad conduit step value %d adjusted to %d.'
        s = w % (steps, asteps)
        golly.warn(s)
        #print s
       
    delta = (asteps - 313) // 4
    #golly.show('Delta=%d'%delta)   

    T = (-delta, delta, identity)   
    pat = hc_tx + hc_rx(*T)
    pat.transform = compose(hc_rx.transform, T)
    return pat

def herschel_track(path, hseq=None):
    ''' Create a track of Herschel conduits.
   
    path is the sequence of conduit names, hseq is a Boolean sequence
    which selects whether to add a true herschel or a herschel ghost
    at the start of the corresponding conduit. If hseq is shorter than
    the path length, it is repeated. A herschel ghost is always added
    to the end of the track.
   
    Return the track pattern and the final transform, to allow tracks
    to be easily chained together. 
    '''
   
    if hseq == None: hseq = (0,)   
   
    #Replicate hseq if it's too short
    lp, lh = len(path), len(hseq)
    if lh < lp:
        hseq *= (lp + lh - 1) // lh
        hseq = hseq[:lp]
       
    #Convert bits in hseq into herschels & ghosts
    herschel_ghost = pattern()
    hlist = [(herschel_ghost, herschel)[i] for i in hseq]

    #Build the track       
    T = (0, 0, identity)   
    track = pattern()   
    for hc, h in zip(path, hlist):
        track += (hc + h)(*T)
        #track += h(*T)     #Just the Herschels / ghosts
        T = compose(hc.transform, T)
         
    track += herschel_ghost(*T)
    return track, T
     
def main():
    rule()
   
    hc521 = herschel_transceiver(521)
    hseq = None
       
    #path = 2*(hc117, ); hseq = (1, 0)     
    #path = 4*(hc112, hc117, ); hseq = (0, 1)  #No internal gliders!   
    #path = 2*(hc112, hc521, hc521, hc112, ); hseq = (1, 0,) 
    #path = 4*(hc521, ); hseq = 2*(1, ) + 2*(0, )
   
    htr = herschel_transceiver(509)
    #path = (herschel_transceiver(401),)
    #path = [herschel_transceiver(393 + 4*i) for i in xrange(8,-1,-1)]

    #Good   
    #path = 4*(hc112, htr, htr)  #401
    #hseq = 4*(1,0,0)
   
    #path = 2*(hc112, htr, htr, htr, htr, hc112, )  #501
   
    path = 2*(hc112, htr, htr, hc112, )  #501
    hseq = 4*(1,0)
   
    track, T = herschel_track(path, hseq) 
       
    #track += hc_tx(*T) 
   
    track.display ("Herschel track")

if __name__ == '__main__':
    main()
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » September 3rd, 2009, 7:19 am

p30 printer
Here's the latest version of my ticker maker script. It can be used to generate patterns like the Golly logo "ticker", or the pattern in my "Marilyn" thread in the Patterns forum. It outputs patterns in the form of a self-extracting compressed Python script, which is a big space saving compared to the .rle format. These output scripts are saved in the same script directory as the script that created them.

The patterns produced by this version use p30 technology (bitmap data is stored using my Memory Loop), so they run faster than earlier versions. They are also more compact.

bm10a.py
# Golly Python script. Written by PM 2Ring, March 2009. Updated August 2009.

''' Create a 'bitmap printer'.

    Print a bitmap in LWSSs, using the current selection as the bitmap source.
    Save output pattern as Python source code.
'''

import zlib, base64, golly
from glife import *

def get_bitmap():
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection, aborting.")

    #Get bitmap size
    w, h = selrect[2:]
    #Adjust width, so it's in the form of 4m, m>1
    w = (w + 3) // 4 * 4
    w = max(8, w)

    #Initialize empty bitmap
    row = w * ['0']
    bm = [row[:] for i in xrange(h)]

    #Populate bitmap with cell data
    u, v = selrect[:2]
    cells = golly.getcells(selrect)
    cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
    for x, y in cellsxy:
        bm[y][x] = '1'
       
    #Convert to CSV string
    return ','.join([''.join(row) for row in bm])

prog = """       
def linemaker(loopm):
    LM = pattern('''34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
$2o11bo2bo7b3o21b2o$13bobo9b2o21b2o$12bobo$12bo9bo$23b2o$22b2o6$47b3o$
24b2o20bo3bo$24b2o19bo5bo$46bo3bo$47b3o$47b3o5$49b3o$44bo3b2ob2o$42b2o
4b2ob2o$35b2o6b2o3b5o$35b2o10b2o3b2o7$45b2o5b2o$45bo6bo$46b3o4b3o$48bo
6bo''')
    LMTop = LM + pattern('2b2o$2bo$obo$2o', 32, 7)
    LMBase = pattern('''11bo$10bo$10b3o12$23b2o$23bobo$10bobo13bo$9bo2bo2b2o6bo2bo12b2o$8b2o5b
obo8bo11b3o$2o4b2o3bo3bo3bo3bobo9bob2o15bo$2o6b2o5b3ob2o2b2o10bo2bo8b
3o4bobo$9bo2bo3b2o17bob2o16bobo$10bobo25b3o2b2o2bo7bo2bo3b2o$39b2o2bo
3bo7bobo4b2o$44bo2bo6bobo$26bo17b2o8bo$24bobo$25b2o2$36bo$36bobo$36b2o
7$33bo7bo$32b4o5bobo$27b2o2bo2b2o8b2o4b2o$27b2o2b2o11b2o4b2o$16b2o6b2o
10bo7b2o$16b2o5b3o10bo4bobo$24b2o10bo4bo$27b2o$27b2o''', 14, 29)
    LMBase += LM(55, 42, flip)
    return LMTop(8 + loopm, -21 - loopm) + LMBase

def main(pdy, copies, title):
    LMsx, pdx = 5, 15
    LMdx, LMdy = -LMsx * pdx, pdy
    bm = [[int(j) for j in i] for i in bits.split(',')]
    golly.new(title)
    golly.setrule("B3/S23")
    bmheight, bmwidth = len(bm), len(bm[0])
    mid = (bmheight + 1) // 2
    loopw = (bmwidth - 8) // 4
    loopm = pdx * loopw
    g0, g1 = pattern('2bo$2o$b2o'), pattern('obo$2o$bo')
    gliders = [g0, g1, g1(0, 2, rccw), g0(0, 2, rccw),
        g0(2, 2, flip), g1(2, 2, flip), g1(2, 0, rcw), g0(2, 0, rcw)]
    gg = []
    ox, oy = 35, 23
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[0](ox + dd, oy - dd), gliders[1](ox + 8 + dd, oy - 7 - dd)]
    dd = loopm
    gg += [gliders[2](45 + dd, 4 - dd), gliders[3](37 + dd, -3 - dd)]
    ox, oy = 26 + loopm, -4 - loopm
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[4](ox - dd, oy + dd), gliders[5](ox - 8 - dd, oy + 7 + dd)]
    dd = loopm
    gg += [gliders[6](16, 15), gliders[7](24, 22)]
    parity = 2*((loopw + 1)*(0, 0) + (1, 1))
    def buildLM():
        gloop = pattern()
        for j in xrange(bmwidth):
            jj = (j - delta + bmwidth - 1) % bmwidth
            if bm[ii][jj] == parity[j]:               
                gloop += gg[j] 
        (LMBlank + gloop).put(Lx, Ly, trans)
   
    LMBlank = linemaker(loopm)
    trans = identity
    for i in xrange(mid):
        ii = mid - (i + 1)
        io = mid + (i + 1)
        Lx, Ly = io * LMdx, ii * LMdy
        delta = LMsx * io
        buildLM()
    trans = flip_y
    for i in xrange(mid, bmheight):
        ii = i
        io = i + 2
        Lx, Ly = io * LMdx + pdx, ii * LMdy + 128
        delta = LMsx * io - 1
        buildLM()
    eaterSE = pattern('2o$bo$bobo$2b2o')
    eaterNE = eaterSE(0, 10, flip_y)
    eY = 59
    eaters = (eaterNE(0, eY), eaterSE(0, eY))
    eX = (bmheight + 1) * LMdx - (copies * bmwidth - 1) * pdx   
    eX = 1 + eX // 2 * 2
    all = pattern()
    for i in xrange(bmheight):
        all += eaters[i % (1 + LMsx % 2)](eX, i * LMdy)
    all.put()   
    golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
    golly.fit()
    golly.setcursor(3)
"""

if __name__  ==  '__main__':   
    title = golly.getname().split('.')[0] + '-printer'
    prog = "bits = '%s'\n%s" % (get_bitmap(), prog)
    exec compile(prog, '<string>', 'exec')
    golly.duplicate()
    golly.setoption("syncviews", False)
    main(16, 1, title)   

    #Save pattern as a Python program   
    s = """#Golly Python script
import zlib,base64,golly\nfrom glife import *
exec compile(zlib.decompress(base64.decodestring('''
%s''')),'<string>','exec')
main(16, 1, '%s')
""" % (base64.encodestring(zlib.compress(prog)), title)   
    f = open(title + '.py', 'w')
    f.write(s)
    f.close()


Here's a smallish sample output in RLE format, for those of you without Golly or Python.

x = 840, y = 1145, rule = B3/S23
440bo$441bo3bo$432b2o2b2o8bo12b2o$432b2o2bo5b2o2bo12b2o$436bobo5b2o$
437b2o3b3o2$437b2o3b3o$436bobo5b2o$436bo5b2o2bo$436b2o8bo$441bo3bo$
440bo2$469b2o$469b2o3$532bo$533bo3bo$524b2o2b2o8bo12b2o$524b2o2bo5b2o
2bo12b2o$462b3o3b3o57bobo5b2o$462bo2bobo2bo58b2o3b3o$461bo3bobo3bo$
461b4o3b4o57b2o3b3o$462bo7bo57bobo5b2o$528bo5b2o2bo$528b2o8bo$533bo3bo
$532bo2$561b2o$469bo91b2o$468b3o$467bo3bo$467b2ob2o152bo$467b2ob2o153b
o3bo$616b2o2b2o8bo12b2o$467b2ob2o144b2o2bo5b2o2bo12b2o$467b2ob2o82b3o
3b3o57bobo5b2o$462b2o3bo3bo82bo2bobo2bo58b2o3b3o$462b2o4b3o82bo3bobo3b
o$469bo83b4o3b4o57b2o3b3o$554bo7bo57bobo5b2o$620bo5b2o2bo$620b2o8bo$
625bo3bo$624bo2$653b2o$561bo91b2o$560b3o$559bo3bo$559b2ob2o54bobo95bo$
559b2ob2o54b2o97bo3bo$619bo88b2o2b2o8bo12b2o$559b2ob2o144b2o2bo5b2o2bo
12b2o$559b2ob2o82b3o3b3o57bobo5b2o$554b2o3bo3bo82bo2bobo2bo58b2o3b3o$
554b2o4b3o82bo3bobo3bo$561bo83b4o3b4o57b2o3b3o$646bo7bo57bobo5b2o$712b
o5b2o2bo$712b2o8bo$717bo3bo$716bo2$745b2o$653bo91b2o$652b3o$651bo3bo$
651b2ob2o152bo$651b2ob2o153bo3bo$800b2o2b2o8bo12b2o$651b2ob2o144b2o2bo
5b2o2bo12b2o$651b2ob2o82b3o3b3o57bobo5b2o$646b2o3bo3bo82bo2bobo2bo58b
2o3b3o$646b2o4b3o82bo3bobo3bo$653bo83b4o3b4o57b2o3b3o$738bo7bo57bobo5b
2o$804bo5b2o2bo$804b2o8bo$517b3o289bo3bo$519bo288bo$518bo$837b2o$745bo
91b2o$744b3o$621b2o120bo3bo$620bobo120b2ob2o$622bo120b2ob2o2$743b2ob2o
$506b2o235b2ob2o82b3o3b3o$505bobo230b2o3bo3bo82bo2bobo2bo$507bo216b3o
11b2o4b3o82bo3bobo3bo$726bo18bo83b4o3b4o$725bo104bo7bo4$826b3o$826bo$
827bo$837bo$494b3o339b3o$496bo216b2o120bo3bo$495bo216bobo120b2ob2o$
714bo120b2ob2o2$835b2ob2o$835b2ob2o$779bobo48b2o3bo3bo$779b2o49b2o4b3o
$780bo56bo3$664bobo$664b2o35b3o$665bo37bo$702bo4$805b2o$804bobo$806bo
26$667b2o$666bobo$668bo5$770b3o$772bo$771bo3$655b3o$657bo$656bo9$644b
2o$643bobo$645bo4$710bobo$710b2o35b3o$711bo37bo$748bo2$595bobo$595b2o$
596bo17$724b3o$726bo$725bo2$572bobo$572b2o35b3o$573bo37bo$610bo8$562bo
$560b2o$561b2o6$701b3o$703bo$702bo8$654bo$652b2o$653b2o11$678b3o$680bo
$679bo2$526bobo$526b2o35b3o$527bo37bo$564bo3$631bo$629b2o$630b2o3$516b
o$514b2o$515b2o2$181bobo$181b2o$182bo6$503bobo$503b2o$504bo$171bo$169b
2o$170b2o$608bo$606b2o36b2o$607b2o34bobo$645bo3$529b2o$528bobo$530bo
10$517b3o$519bo$518bo3$585bo$583b2o$584b2o3$470bo$468b2o36b2o$469b2o
34bobo$507bo5$609b3o$611bo$610bo2$457bobo$457b2o$458bo9$447bo$445b2o$
446b2o11$471b3o$473bo$472bo3$539bo$537b2o36b2o$538b2o34bobo$576bo$204b
obo$204b2o$205bo18$552b2o$551bobo$553bo3$437b2o$436bobo$438bo19$158bob
o$158b2o218bo$159bo216b2o36b2o$377b2o34bobo$415bo4$8b2o470bobo$8b2o
470b2o$481bo3$365bobo$365b2o$366bo3$250bobo$250b2o$251bo2$b2o5b2o$o2bo
3bo2bo$bo2bobo2bo345bo$4bobo346b2o$2b3ob3o91b2o252b2o$3o5b3o89b2o$2o7b
2o$2o7b2o$bob2ob2obo266b2o$b3o3b3o265bobo$2bo5bo268bo$10bo5bo$9b3o3b3o
$9bob2ob2obo$8b2o7b2o$8b2o7b2o$8b3o5b3o$10b3ob3o$12bobo78b2o5b2o$9bo2b
obo2bo74bo2bo3bo2bo$8bo2bo3bo2bo74bo2bobo2bo$9b2o5b2o78bobo$94b3ob3o
91b2o$92b3o5b3o89b2o$92b2o7b2o$92b2o7b2o$93bob2ob2obo$93b3o3b3o$94bo5b
o$102bo5bo$101b3o3b3o$28b2o71bob2ob2obo$28b2o70b2o7b2o323bobo$100b2o7b
2o323b2o35b3o$16b2o82b3o5b3o324bo37bo$16b2o84b3ob3o363bo$104bobo31b2o
45b2o5b2o$101bo2bobo2bo27bobo44bo2bo3bo2bo124bobo$100bo2bo3bo2bo28bo
45bo2bobo2bo125b2o$101b2o5b2o78bobo129bo$186b3ob3o91b2o$184b3o5b3o89b
2o$184b2o7b2o$87b2o95b2o7b2o$30bo3bo52b2o96bob2ob2obo$30bo3bo150b3o3b
3o$186bo5bo$194bo5bo$27b2obo3bob2o155b3o3b3o107bo$28b3o3b3o43b3o3b3o
31b2o71bob2ob2obo105b2o36b2o$29bo5bo44bo2bobo2bo31b2o70b2o7b2o105b2o
34bobo$79bo3bobo3bo102b2o7b2o143bo$80bo2bobo2bo19b2o82b3o5b3o$82bo3bo
21b2o84b3ob3o$80b2o5b2o107bobo78b2o5b2o$79b3o5b3o103bo2bobo2bo74bo2bo
3bo2bo$79b3o5b3o102bo2bo3bo2bo74bo2bobo2bo$193b2o5b2o78bobo$35b2o241b
3ob3o91b2o$35b2o239b3o5b3o89b2o$276b2o7b2o9bobo$80b3o96b2o95b2o7b2o9b
2o35b3o$80b3o39bo3bo52b2o96bob2ob2obo11bo37bo$79b5o38bo3bo150b3o3b3o
48bo$78b2o3b2o193bo5bo$78b2o3b2o201bo5bo$119b2obo3bob2o155b3o3b3o$120b
3o3b3o43b3o3b3o31b2o71bob2ob2obo$121bo5bo44bo2bobo2bo31b2o70b2o7b2o$
78b2o3b2o86bo3bobo3bo102b2o7b2o$78b2o3b2o2b2o83bo2bobo2bo19b2o82b3o5b
3o$79b5o3b2o85bo3bo21b2o84b3ob3o$80b3o89b2o5b2o107bobo78b2o5b2o$80b3o
88b3o5b3o103bo2bobo2bo74bo2bo3bo2bo$171b3o5b3o102bo2bo3bo2bo5bo68bo2bo
bo2bo$285b2o5b2o7b2o69bobo$127b2o171b2o68b3ob3o$127b2o239b3o5b3o$368b
2o7b2o9bobo$172b3o96b2o95b2o7b2o9b2o35b3o$172b3o39bo3bo52b2o96bob2ob2o
bo11bo37bo$171b5o38bo3bo150b3o3b3o48bo$170b2o3b2o193bo5bo$170b2o3b2o
201bo5bo$211b2obo3bob2o155b3o3b3o$212b3o3b3o43b3o3b3o31b2o71bob2ob2obo
$213bo5bo44bo2bobo2bo31b2o70b2o7b2o$170b2o3b2o86bo3bobo3bo102b2o7b2o$
170b2o3b2o2b2o83bo2bobo2bo19b2o82b3o5b3o$171b5o3b2o85bo3bo21b2o84b3ob
3o$172b3o89b2o5b2o107bobo$172b3o88b3o5b3o103bo2bobo2bo$263b3o5b3o102bo
2bo3bo2bo$377b2o5b2o$219b2o$219b2o2$264b3o96b2o$264b3o39bo3bo52b2o$
263b5o38bo3bo$262b2o3b2o$262b2o3b2o$303b2obo3bob2o$304b3o3b3o43b3o3b3o
31b2o$305bo5bo44bo2bobo2bo31b2o$262b2o3b2o86bo3bobo3bo$262b2o3b2o2b2o
83bo2bobo2bo19b2o$263b5o3b2o85bo3bo21b2o$264b3o89b2o5b2o$264b3o88b3o5b
3o$355b3o5b3o2$311b2o$311b2o2$356b3o96b2o$356b3o39bo3bo52b2o$355b5o38b
o3bo$354b2o3b2o$354b2o3b2o$395b2obo3bob2o$396b3o3b3o43b3o3b3o$397bo5bo
44bo2bobo2bo$354b2o3b2o86bo3bobo3bo$354b2o3b2o2b2o83bo2bobo2bo$355b5o
3b2o85bo3bo$356b3o89b2o5b2o$356b3o88b3o5b3o$447b3o5b3o2$403b2o$403b2o
2$448b3o$448b3o$447b5o$446b2o3b2o$446b2o3b2o4$446b2o3b2o$446b2o3b2o2b
2o$447b5o3b2o$448b3o$448b3o2$448b3o$448b3o$447b5o3b2o$446b2o3b2o2b2o$
446b2o3b2o4$446b2o3b2o$446b2o3b2o$447b5o$448b3o$448b3o2$403b2o$403b2o
2$447b3o5b3o$356b3o88b3o5b3o$356b3o89b2o5b2o$355b5o3b2o85bo3bo$354b2o
3b2o2b2o83bo2bobo2bo$354b2o3b2o86bo3bobo3bo$397bo5bo44bo2bobo2bo$396b
3o3b3o43b3o3b3o$395b2obo3bob2o$354b2o3b2o$354b2o3b2o$355b5o38bo3bo$
356b3o39bo3bo52b2o$356b3o96b2o2$311b2o$311b2o2$355b3o5b3o$264b3o88b3o
5b3o$264b3o89b2o5b2o$263b5o3b2o85bo3bo21b2o$262b2o3b2o2b2o83bo2bobo2bo
19b2o$262b2o3b2o86bo3bobo3bo$305bo5bo44bo2bobo2bo31b2o$304b3o3b3o43b3o
3b3o31b2o$303b2obo3bob2o$262b2o3b2o$262b2o3b2o$263b5o38bo3bo$264b3o39b
o3bo52b2o$264b3o96b2o2$219b2o$219b2o$377b2o5b2o$263b3o5b3o102bo2bo3bo
2bo$172b3o88b3o5b3o103bo2bobo2bo$172b3o89b2o5b2o107bobo$171b5o3b2o85bo
3bo21b2o84b3ob3o$170b2o3b2o2b2o83bo2bobo2bo19b2o82b3o5b3o$170b2o3b2o
86bo3bobo3bo102b2o7b2o$213bo5bo44bo2bobo2bo31b2o70b2o7b2o$212b3o3b3o
43b3o3b3o31b2o71bob2ob2obo$211b2obo3bob2o155b3o3b3o$170b2o3b2o201bo5bo
$170b2o3b2o193bo5bo$171b5o38bo3bo150b3o3b3o48bo$172b3o39bo3bo52b2o96bo
b2ob2obo11bo37bo$172b3o96b2o95b2o7b2o9b2o35b3o$368b2o7b2o9bobo$127b2o
239b3o5b3o$127b2o241b3ob3o$285b2o5b2o78bobo$171b3o5b3o102bo2bo3bo2bo
74bo2bobo2bo$80b3o88b3o5b3o103bo2bobo2bo74bo2bo3bo2bo$80b3o89b2o5b2o
107bobo78b2o5b2o$79b5o3b2o85bo3bo21b2o84b3ob3o$78b2o3b2o2b2o83bo2bobo
2bo19b2o82b3o5b3o$78b2o3b2o86bo3bobo3bo102b2o7b2o$121bo5bo44bo2bobo2bo
31b2o70b2o7b2o105b2o$120b3o3b3o43b3o3b3o31b2o71bob2ob2obo105b2o$119b2o
bo3bob2o155b3o3b3o107bo$78b2o3b2o201bo5bo$78b2o3b2o193bo5bo$79b5o38bo
3bo150b3o3b3o48bo$80b3o39bo3bo52b2o96bob2ob2obo11bo37bo$80b3o96b2o95b
2o7b2o9b2o35b3o$276b2o7b2o9bobo$35b2o239b3o5b3o89b2o$35b2o171b2o68b3ob
3o91b2o$193b2o5b2o7b2o69bobo129bo$79b3o5b3o102bo2bo3bo2bo5bo22bo45bo2b
obo2bo125b2o$79b3o5b3o103bo2bobo2bo27bobo44bo2bo3bo2bo124bobo$80b2o5b
2o107bobo31b2o45b2o5b2o$82bo3bo21b2o84b3ob3o$80bo2bobo2bo19b2o82b3o5b
3o$79bo3bobo3bo102b2o7b2o$29bo5bo44bo2bobo2bo31b2o70b2o7b2o$28b3o3b3o
43b3o3b3o31b2o71bob2ob2obo$27b2obo3bob2o155b3o3b3o$194bo5bo$186bo5bo$
30bo3bo150b3o3b3o48bo180b2o$30bo3bo52b2o96bob2ob2obo11bo37bo178b2o$87b
2o95b2o7b2o9b2o35b3o180bo$184b2o7b2o9bobo$184b3o5b3o89b2o$186b3ob3o91b
2o$101b2o5b2o78bobo$100bo2bo3bo2bo74bo2bobo2bo$101bo2bobo2bo74bo2bo3bo
2bo$104bobo78b2o5b2o$16b2o84b3ob3o363bo$16b2o82b3o5b3o324bo37bo$100b2o
7b2o143bo179b2o35b3o$28b2o5bo64b2o7b2o105b2o34bobo179bobo$28b2o6bo64bo
b2ob2obo105b2o36b2o$34b3o64b3o3b3o107bo$102bo5bo$94bo5bo$93b3o3b3o$93b
ob2ob2obo$92b2o7b2o$92b2o7b2o$92b3o5b3o89b2o$94b3ob3o91b2o71bo$9b2o5b
2o78bobo167bo$8bo2bo3bo2bo28bo45bo2bobo2bo162b3o$9bo2bobo2bo27bobo44bo
2bo3bo2bo$12bobo31b2o45b2o5b2o$10b3ob3o$8b3o5b3o324bo$8b2o7b2o143bo
179b2o$8b2o7b2o141bobo179bobo$9bob2ob2obo143b2o$9b3o3b3o$10bo5bo$2bo5b
o268bo$b3o3b3o265bobo$bob2ob2obo266b2o$2o7b2o$2o7b2o$3o5b3o89b2o$2b3ob
3o91b2o$4bobo$bo2bobo2bo$o2bo3bo2bo$b2o5b2o2$251bo$250b2o$250bobo8$
481bo$8b2o290bo179b2o$8b2o288bobo179bobo$299b2o3$415bo$413bobo$414b2o
4$311bo$312bo$310b3o10$323bo$321bobo$322b2o3$438bo$436bobo$437b2o3$
553bo$551bobo$552b2o7$193b2o$192b2o$194bo3$308b2o$307b2o$309bo7$576bo$
357bo180b2o34bobo$320bo37bo178b2o36b2o$319b2o35b3o180bo$319bobo2$472bo
$435bo37bo$434b2o35b3o$434bobo4$369bo$331b2o34bobo$330b2o36b2o$332bo8$
380bo$343bo37bo$342b2o35b3o$342bobo7$610bo$611bo$392bo216b3o$390bobo$
391b2o3$507bo$505bobo$506b2o4$584b2o$366bo216b2o$365b2o218bo$365bobo
15$492b2o$491b2o$493bo2$645bo$607b2o34bobo$606b2o36b2o$608bo3$541bo$
542bo$540b3o5$438bo$400b2o34bobo$399b2o36b2o$401bo8$630b2o$412bo216b2o
$411b2o218bo$411bobo7$679bo$680bo$461bo216b3o$423b2o34bobo$422b2o36b2o
$424bo2$576bo$538b2o34bobo$537b2o36b2o$539bo3$653b2o$652b2o$654bo8$
702bo$703bo$701b3o5$599bo$561b2o34bobo$560b2o36b2o$562bo3$495bo$458bo
37bo$457b2o35b3o$457bobo7$725bo$726bo$724b3o$469b2o$468b2o$470bo3$584b
2o$583b2o$585bo4$481bo$480b2o$480bobo2$633bo$634bo$632b3o3$748bo$711bo
37bo$530bo179b2o35b3o$528bobo179bobo$529b2o4$607b2o$606b2o$608bo13$
771bo$772bo$770b3o$515b2o$514b2o$516bo2$668bo$666bobo$667b2o4$564bo$
565bo$563b3o10$576bo$538b2o34bobo$537b2o36b2o$539bo2$691bo$689bobo$
690b2o3$806bo$587bo216bobo$588bo216b2o$586b3o4$665bo$664b2o$664bobo3$
780bo56bo$779b2o49b2o4b3o$561b2o216bobo48b2o3bo3bo$560b2o273b2ob2o$
562bo272b2ob2o2$835b2ob2o$835b2ob2o$835bo3bo$836b3o$837bo$827bo$826bo$
826b3o4$830bo7bo$745bo83b4o3b4o$738b2o4b3o82bo3bobo3bo$738b2o3bo3bo82b
o2bobo2bo$743b2ob2o82b3o3b3o$743b2ob2o2$743b2ob2o$743b2ob2o$743bo3bo$
744b3o$745bo91b2o$735bo101b2o$734bo$734b3o71bo$809bo3bo$804b2o8bo$804b
o5b2o2bo$633bo104bo7bo57bobo5b2o$634bo18bo83b4o3b4o57b2o3b3o$632b3o11b
2o4b3o82bo3bobo3bo$646b2o3bo3bo82bo2bobo2bo58b2o3b3o$651b2ob2o82b3o3b
3o57bobo5b2o$651b2ob2o144b2o2bo5b2o2bo12b2o$711bo88b2o2b2o8bo12b2o$
651b2ob2o54b2o97bo3bo$651b2ob2o54bobo95bo$651bo3bo$652b3o$653bo91b2o$
643bo101b2o$642bo$642b3o71bo$717bo3bo$712b2o8bo$712bo5b2o2bo$646bo7bo
57bobo5b2o$561bo83b4o3b4o57b2o3b3o$554b2o4b3o82bo3bobo3bo$554b2o3bo3bo
82bo2bobo2bo58b2o3b3o$559b2ob2o82b3o3b3o57bobo5b2o$559b2ob2o144b2o2bo
5b2o2bo12b2o$631bo76b2o2b2o8bo12b2o$559b2ob2o67bobo83bo3bo$559b2ob2o
67b2o83bo$559bo3bo$560b3o$561bo91b2o$653b2o2$624bo$625bo3bo$620b2o8bo$
620bo5b2o2bo$554bo7bo57bobo5b2o$469bo83b4o3b4o57b2o3b3o$462b2o4b3o82bo
3bobo3bo$462b2o3bo3bo82bo2bobo2bo58b2o3b3o$467b2ob2o82b3o3b3o57bobo5b
2o$467b2ob2o144b2o2bo5b2o2bo12b2o$616b2o2b2o8bo12b2o$467b2ob2o153bo3bo
$467b2ob2o152bo$467bo3bo$468b3o$469bo91b2o$561b2o2$532bo$533bo3bo$528b
2o8bo$528bo5b2o2bo$462bo7bo57bobo5b2o$461b4o3b4o57b2o3b3o$461bo3bobo3b
o$462bo2bobo2bo58b2o3b3o$462b3o3b3o57bobo5b2o$524b2o2bo5b2o2bo12b2o$
524b2o2b2o8bo12b2o$533bo3bo$532bo3$469b2o$469b2o2$440bo$441bo3bo$436b
2o8bo$436bo5b2o2bo$436bobo5b2o$437b2o3b3o2$437b2o3b3o$436bobo5b2o$432b
2o2bo5b2o2bo12b2o$432b2o2b2o8bo12b2o$441bo3bo$440bo!


EDIT
Please see below for a minor update to this script, which fixes a bug that will prevent it from working on more recent versions of Golly.
Last edited by PM 2Ring on September 14th, 2009, 7:48 am, edited 1 time in total.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » September 3rd, 2009, 7:49 am

HTML creator for Life pattern collections

Many of the patterns in the various pattern collections contain very useful comments. This script makes them much more accessible, IMHO.
PM 2Ring wrote:I'm currently working on a script that trawls through a directory (& any subdirectories) looking for any Life pattern files that Golly knows how to load & builds a set of HTML pages (one per directory) containing all the comments it finds in these pattern files, as well as the patterns themselves in the same format used by the Life Lexicon (if they aren't too wide). Clicking on the filename opens a new page with the contents of the pattern file, which is handy for the larger patterns.

Sorry about the delay. I ran out of enthusiasm on this project, but I finally managed to motivate myself to do some more work on it, so it's now ready for release. As well as the info mentioned above, each pattern entry also lists the pattern width & height, population, and density. The generated HTML pages have a lilac background, to distinguish them from the Lexicon pages.

The script puts the HTML files it generates into a Patterns/ directory of the Golly Help/ directory. It will create the Patterns/ directory if it doesn't exist. Each HTML file will be given a name based on the directory it was created from. The script also creates & maintain a master index.htm file that links all the top directory pages together. If you want to be able to access these files from within Golly, you will need to edit the Life Lexicon lex.htm file to create a link to this index file, adding a line like this near the top (&/or bottom) of the file:
<a href="../Patterns/index.htm">Pattern Collections</a>

(I guess you could also link it into any of the other Golly Help files, but I think the Lexicon's the best place for it.) If you link these files into Golly like this, you will be able to load patterns directly by clicking on them, just like the Life Lexicon patterns. Of course, you can also view these files using any HTML browser, but you then lose the auto-load feature.

This script will happily process your entire pattern collection (if it all lives under one directory), but it will also work properly if you just want it to process one directory at a time (as long as you don't mess up the footer lines of the master index.htm file).

makeHTML2.py
# Golly python script.
# Written by PM 2Ring 2009.7.4

''' Create HTML page(s) from a directory of Life patterns.

    Can only handle pattern types that Golly can load; doesn't handle image file formats.
    Recursively descends into directories.

    Output is in a similar format to the Life Lexicon, so displayed patterns
    can be loaded directly into Golly.

'''

from glife import *
import golly

import pygtk
pygtk.require('2.0')
import gtk

import os, re, time

def getdirname(title='Choose folder...', folder=''):
    ''' Get a folder name from folder '''
    dialog = gtk.FileChooserDialog(title, None,
        gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
        gtk.STOCK_OPEN, gtk.RESPONSE_OK))
    dialog.set_current_folder(folder)     #else it opens in script folder

    fname = dialog.run() == gtk.RESPONSE_OK and dialog.get_filename()
    dialog.destroy()
    return fname

class bitmap:
    ''' Basic pixel array, with text output '''
    def __init__(self, x, y, bitchars='.O'):
        self.x, self.y, self.bitchars = x, y, bitchars

        #Pixel buffer.
        row = x * [self.bitchars[0]]
        self.pix = [row[:] for i in xrange(y)]

    #Put pixel in buffer.
    def put(self, x, y, c=1):
        self.pix[y][x] = self.bitchars[c]

    def __str__(self):
        return '\n'.join([''.join(row) for row in self.pix])

def totext(patt, width, height):
    ''' Convert pattern to '.O' text format '''
    bm = bitmap(width, height, bitchars='.O')
    cellsxy = [(patt[i], patt[i+1]) for i in xrange(0, len(patt), 2)]
    for x, y in cellsxy:
        bm.put(x, y)
    return str(bm)

def process(patdir, htmdir, maxwidth = 130):
    ''' Search for pattern files in patdir & create HTML catalogue in htmdir '''
    lexpath = golly.appdir() + 'Help/Lexicon/lex.htm'
    lexicon = '<center><A HREF="%s">Back to Life Lexicon</A></center>' % lexpath

    #HTML file header
    def header(rname):       
        title = 'Life patterns in %s' % rname
        s = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="en">
<head>
<title>%s</title>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
<link href="lifelex.css" rel="stylesheet" type="text/css">
<link rel="begin" type="text/html" href="%s" title="Life Lexicon">
</head>
<body bgcolor="#FFCEFF">%s
<hr>\n<center><h2>%s</h2></center>\n''' % (title, lexpath, lexicon, title)
        htm.write(s)
        if rname == patbase:
            s = '''<A HREF="index.htm">Back to Pattern Collection index</A>
<p>These pages catalogue all Life pattern files found in %s
and its subdirectories.<br><br>\n''' % patdir
        else:
            s = '<A HREF="%s.htm">Back to %s</A>\n' % (patbase, patbase)
        htm.write(s)

    #HTML index file header
    def iheader():
        title = 'Life patterns'
        s = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="en">
<head>
<title>%s</title>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
<link href="lifelex.css" rel="stylesheet" type="text/css">
<link rel="begin" type="text/html" href="%s" title="Life Lexicon">
</head>
<body bgcolor="#FFCEFF">%s
<hr>\n<center><h1>%s</h1></center>\n''' % (title, lexpath, lexicon, title)
        htm.write(s)
   
        s = '''<p> This is the master index page for Life pattern collection info. It was
generated using makeHTML, a Golly Python script written by PM 2Ring.<BR><BR>\n
<h2>Pattern collections</h2>\n'''
        htm.write(s)
   
        s = '<A HREF="%s.htm">%s</A><BR>\n' % (patbase, patbase)
        htm.write(s)

    def footer():
        t = time.strftime('%X, %d %b %Y %Z')
        s = '''\n%s\n<P style="font-size: 8pt;">Page created: %s
<hr></body>\n</html>\n''' % (lexicon, t)
        htm.write(s)

#Format string for pattern text
    fmt = '<center><table cellspacing=0 cellpadding=0><tr><td>\
<pre><a href="lexpatt:">\n%s\n</a></pre></td></tr></table></center>\n'

    #Case-insensitive comparison function for sorting file names
    icmp = lambda x,y: cmp(x.lower(), y.lower())
    #Regular expression for matching comments in pattern file
    renotes = re.compile('#[CD][^X].*')
    #Regular expression for matching Life file extensions
    relife = re.compile(r'.*\.(rle|lif|l|mc)$', re.I)

    golly.show('Looking for Life pattern files in %s...' % patdir)

    patbase = os.path.basename(patdir)
    #print 'patbase =', patbase

    #Create or update index file.
    fname = os.path.join(htmdir, 'index') + '.htm'
    if not os.access(fname, os.F_OK):
        htm = open(fname, 'w')
        iheader()
        footer()
        htm.close()
    else:
        htm = open(fname, 'r')
        index_text = htm.readlines()[:-5]   
        htm.close()
        index_text += ['<A HREF="%s.htm">%s</A><BR>\n' % (patbase, patbase)]
        htm = open(fname, 'w')
        for s in index_text:
            htm.write(s)
        footer()
        htm.close()

    for root, dirs, files in os.walk(patdir):
        rname = os.path.basename(root)

        #print 'root=%s\ndirs=%s\nfiles=%s\n' % (root, dirs, files)
        #print 'root=%s' % root

        dirs.sort(icmp)
        #Filter non-Life files out of file list
        sfiles = [i for i in files if relife.match(i)]
        sfiles.sort(icmp)

        #print 'Dirs in %s\n%s\n' % (rname, dirs)
        #print 'Files in %s\n%s\n' % (rname, sfiles)

        #Open output file & write header
        fname = os.path.join(htmdir, rname) + '.htm'
        htm = open(fname, 'w')
        header(rname)

        if dirs:
            htm.write('<h3>Subdirectories<h3>\n')
            for name in dirs:
                htm.write('<a href="%s.htm">%s</a><br>\n' % (name, name))
            htm.write('<hr>\n')

        if sfiles:
            htm.write('<h3>Files<h3>\n')
            for name in sfiles:
                fname = os.path.join(root, name)
                bname = os.path.join(rname, name)
                golly.show(bname)
                htm.write('<p><a href="%s" name="%s"><b>%s</b></a><br>\n' % (fname, name, bname))
                #print '\n', bname

                #Read pattern data
                patt = load(fname)
                bbox = getminbox(patt)
                width, height = bbox[2:]
                pop = len(patt) // 2
                htm.write('Size = %d x %d, Population = %d, ' % (width, height, pop))
                htm.write('Density = %f<br>\n' % (pop / float(width * height)))
                #print 'Size = %d x %d, Population = %d' % (width, height, pop)

                if 1:
                    #Extract comments
                    f = open(fname, 'r')
                    s = f.readlines()
                    f.close()
                    notes = ''.join([i[2:] for i in s if renotes.match(i)])
                    if notes:
                        htm.write('<pre>%s</pre>\n' % notes)
                        #print notes

                if width <= maxwidth:
                    #Put pattern in 'dot-O' format
                    text = totext(list(patt), width, height)
                    htm.write(fmt % text)
                elif 0:
                    #Put pattern in RLE format. Not recommended: it takes up a lot
                    # of space on the page & pages take much longer to load.
                    s = [i for i in s if not renotes.match(i)]
                    for i in xrange(1, len(s)):
                        if i % 4:
                            s[i] = s[i][:-1]
                    text = ''.join(s)
                    htm.write(fmt % text)

            htm.write('<hr>\n')

        footer()
        htm.close()

    golly.show('Finished.')

def main():
    #Get pattern dir to process   
    folder = golly.appdir() + 'Patterns'
    patdir = getdirname("Choose pattern folder to process", folder)
    if not patdir:
        golly.exit('No folder selected, aborting.')
        #patdir = golly.appdir() + 'Patterns/Test'

    #Directory for generated HTML files
    htmdir = golly.appdir() + 'Help/Patterns/'
    maxwidth = 130  #Maximum pattern width to print

    #If dir doesn't exist, create it
    if not os.access(htmdir, os.F_OK):
        os.mkdir(htmdir)
        golly.show('%s created' % htmdir)

    process(patdir, htmdir, maxwidth)

main()


As always, any comments or suggestions are most welcome.
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby PM 2Ring » September 14th, 2009, 8:03 am

p30 printer: update

This version comments out the setcursor() call, which fails on recent versions of Golly. It will also allow you to recover the uncompressed version of the compressed output script: simply run the output script at the command line instead of in Golly & the script will be printed to the terminal, so you can redirect the output to a file if you want to modify it. Note that the bitmap pattern is stored in one big string at the top of the script. This might cause problems with some editors if the bitmap is large, but it doesn't seem to phase Golly - I just tested it with a 332 x 341 (= 113212 pixels) image, and everything worked fine.

bm10b.py
# Golly Python script. Written by PM 2Ring, March 2009. Updated August 2009.

''' Create a 'bitmap printer'.

    Print a bitmap in LWSSs, using the current selection as the bitmap source.
    Save output pattern as Python source code.
'''

import zlib, base64, golly
from glife import *

def get_bitmap():
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection, aborting.")

    #Get bitmap size
    w, h = selrect[2:]
    #Adjust width, so it's in the form of 4m, m>1
    w = (w + 3) // 4 * 4
    w = max(8, w)

    #Initialize empty bitmap
    row = w * ['0']
    bm = [row[:] for i in xrange(h)]

    #Populate bitmap with cell data
    u, v = selrect[:2]
    cells = golly.getcells(selrect)
    cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
    for x, y in cellsxy:
        bm[y][x] = '1'
       
    #Convert to CSV string
    return ','.join([''.join(row) for row in bm])

prog = """       
def linemaker(loopm):
    LM = pattern('''34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
$2o11bo2bo7b3o21b2o$13bobo9b2o21b2o$12bobo$12bo9bo$23b2o$22b2o6$47b3o$
24b2o20bo3bo$24b2o19bo5bo$46bo3bo$47b3o$47b3o5$49b3o$44bo3b2ob2o$42b2o
4b2ob2o$35b2o6b2o3b5o$35b2o10b2o3b2o7$45b2o5b2o$45bo6bo$46b3o4b3o$48bo
6bo''')
    LMTop = LM + pattern('2b2o$2bo$obo$2o', 32, 7)
    LMBase = pattern('''11bo$10bo$10b3o12$23b2o$23bobo$10bobo13bo$9bo2bo2b2o6bo2bo12b2o$8b2o5b
obo8bo11b3o$2o4b2o3bo3bo3bo3bobo9bob2o15bo$2o6b2o5b3ob2o2b2o10bo2bo8b
3o4bobo$9bo2bo3b2o17bob2o16bobo$10bobo25b3o2b2o2bo7bo2bo3b2o$39b2o2bo
3bo7bobo4b2o$44bo2bo6bobo$26bo17b2o8bo$24bobo$25b2o2$36bo$36bobo$36b2o
7$33bo7bo$32b4o5bobo$27b2o2bo2b2o8b2o4b2o$27b2o2b2o11b2o4b2o$16b2o6b2o
10bo7b2o$16b2o5b3o10bo4bobo$24b2o10bo4bo$27b2o$27b2o''', 14, 29)
    LMBase += LM(55, 42, flip)
    return LMTop(8 + loopm, -21 - loopm) + LMBase

def main(pdy, copies, title):
    LMsx, pdx = 5, 15
    LMdx, LMdy = -LMsx * pdx, pdy
    bm = [[int(j) for j in i] for i in bits.split(',')]
    golly.new(title)
    golly.setrule("B3/S23")
    bmheight, bmwidth = len(bm), len(bm[0])
    mid = (bmheight + 1) // 2
    loopw = (bmwidth - 8) // 4
    loopm = pdx * loopw
    g0, g1 = pattern('2bo$2o$b2o'), pattern('obo$2o$bo')
    gliders = [g0, g1, g1(0, 2, rccw), g0(0, 2, rccw),
        g0(2, 2, flip), g1(2, 2, flip), g1(2, 0, rcw), g0(2, 0, rcw)]
    gg = []
    ox, oy = 35, 23
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[0](ox + dd, oy - dd), gliders[1](ox + 8 + dd, oy - 7 - dd)]
    dd = loopm
    gg += [gliders[2](45 + dd, 4 - dd), gliders[3](37 + dd, -3 - dd)]
    ox, oy = 26 + loopm, -4 - loopm
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[4](ox - dd, oy + dd), gliders[5](ox - 8 - dd, oy + 7 + dd)]
    dd = loopm
    gg += [gliders[6](16, 15), gliders[7](24, 22)]
    parity = 2*((loopw + 1)*(0, 0) + (1, 1))
    def buildLM():
        gloop = pattern()
        for j in xrange(bmwidth):
            jj = (j - delta + bmwidth - 1) % bmwidth
            if bm[ii][jj] == parity[j]:               
                gloop += gg[j] 
        (LMBlank + gloop).put(Lx, Ly, trans)
   
    LMBlank = linemaker(loopm)
    trans = identity
    for i in xrange(mid):
        ii = mid - (i + 1)
        io = mid + (i + 1)
        Lx, Ly = io * LMdx, ii * LMdy
        delta = LMsx * io
        buildLM()
    trans = flip_y
    for i in xrange(mid, bmheight):
        ii = i
        io = i + 2
        Lx, Ly = io * LMdx + pdx, ii * LMdy + 128
        delta = LMsx * io - 1
        buildLM()
    eaterSE = pattern('2o$bo$bobo$2b2o')
    eaterNE = eaterSE(0, 10, flip_y)
    eY = 59
    eaters = (eaterNE(0, eY), eaterSE(0, eY))
    eX = (bmheight + 1) * LMdx - (copies * bmwidth - 1) * pdx   
    eX = 1 + eX // 2 * 2
    all = pattern()
    for i in xrange(bmheight):
        all += eaters[i % (1 + LMsx % 2)](eX, i * LMdy)
    all.put()   
    golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
    golly.fit()
    #golly.setcursor(3)
"""

if __name__  ==  '__main__':   
    title = golly.getname().split('.')[0] + '-printer'
    prog = "bits = '%s'\n%s" % (get_bitmap(), prog)
    exec compile(prog, '<string>', 'exec')
    golly.duplicate()
    golly.setoption("syncviews", False)
    main(16, 1, title)   

    #Save pattern as a Python program
    hdr = '#Golly Python script\nimport golly\nfrom glife import *\n'
    ftr = "main(16, 1, '%s')" % title 
    s = """#Golly Python script
import zlib,base64
s=zlib.decompress(base64.decodestring('''
%s'''))
try:import golly
except ImportError:print s
else:   
from glife import *
exec compile(s,'<string>','exec')
main(16, 1, '%s')
""" % (base64.encodestring(zlib.compress(hdr + prog + ftr)), title)   
    f = open(title + '.py', 'w')
    f.write(s)
    f.close()     


Here's the original "straight" version of the above script. It doesn't have the ability to output the pattern as a Python script. As well as being a lot easier to read than the above, especially if you read it in an editor with syntax highlighting, it also has comments, which (hopefully) explain what's going on. :)

bm10.py
# Golly Python script. Written by PM 2Ring, March 2009. Updated August 2009.

''' Create a 'bitmap printer'.

    Print a bitmap in LWSSs, using the current selection as the bitmap source.
'''

import golly
from glife import *

def get_bitmap():
    ''' Get current selection & turn it into a bitmap. '''
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection, aborting.")

    #Get bitmap size
    w, h = selrect[2:]
    #Adjust width, so it's in the form of 4m, m>1
    w = (w + 3) // 4 * 4
    w = max(8, w)

    #Initialize empty bitmap
    row = w * [0]
    bm = [row[:] for i in xrange(h)]

    #Populate bitmap with cell data
    u, v = selrect[:2]
    cells = golly.getcells(selrect)
    cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
    for x, y in cellsxy:
        bm[y][x] = 1
    return bm

def linemaker(loopm):
    ''' Create an empty LineMaker '''
    LM = pattern('''x = 56, y = 43
34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
$2o11bo2bo7b3o21b2o$13bobo9b2o21b2o$12bobo$12bo9bo$23b2o$22b2o6$47b3o$
24b2o20bo3bo$24b2o19bo5bo$46bo3bo$47b3o$47b3o5$49b3o$44bo3b2ob2o$42b2o
4b2ob2o$35b2o6b2o3b5o$35b2o10b2o3b2o7$45b2o5b2o$45bo6bo$46b3o4b3o$48bo
6bo!''')
    LMTop = LM + pattern('2b2o$2bo$obo$2o!', 32, 7)
    LMBase = pattern('''x = 64, y = 47
11bo$10bo$10b3o12$23b2o$23bobo$10bobo13bo$9bo2bo2b2o6bo2bo12b2o$8b2o5b
obo8bo11b3o$2o4b2o3bo3bo3bo3bobo9bob2o15bo$2o6b2o5b3ob2o2b2o10bo2bo8b
3o4bobo$9bo2bo3b2o17bob2o16bobo$10bobo25b3o2b2o2bo7bo2bo3b2o$39b2o2bo
3bo7bobo4b2o$44bo2bo6bobo$26bo17b2o8bo$24bobo$25b2o2$36bo$36bobo$36b2o
7$33bo7bo$32b4o5bobo$27b2o2bo2b2o8b2o4b2o$27b2o2b2o11b2o4b2o$16b2o6b2o
10bo7b2o$16b2o5b3o10bo4bobo$24b2o10bo4bo$27b2o$27b2o!''', 14, 29)
    LMBase += LM(55, 42, flip)
    return LMTop(8 + loopm, -21 - loopm) + LMBase

def dmprinter(pdy, copies=1):
    ''' Generate & display a dot matrix printer for named bitmap pattern '''
    #Horizontal pixel separation between LineMakers. minimum = 5
    LMsx = 5

    #Horizontal distance between bitmap pixels. Constant, due to LineMaker period
    pdx = 15

    #Distance between LineMakers
    LMdx, LMdy = -LMsx * pdx, pdy

    #Get bitmap pattern from current selection.
    bm = get_bitmap()

    #Make printer in a new layer
    golly.duplicate()
    golly.setoption("syncviews", False)

    #Build new window title from old
    title = golly.getname().split('.')[0]
    title = '%s_Printer [%d] v0.10' % (title, pdy)
    golly.new(title)

    #Make sure we're using the standard Life generation rule
    golly.setrule("B3/S23")

    #Bitmap dimensions. Width MUST be of form 4m, for m >=1
    bmheight = len(bm)
    bmwidth = len(bm[0])
    mid = (bmheight + 1) // 2

    loopw = (bmwidth - 8) // 4
    loopm = pdx * loopw

    #Gliders, heading north-east
    g0 = pattern('2bo$2o$b2o!')
    g1 = pattern('obo$2o$bo!')
    gliders = [g0, g1, g1(0, 2, rccw), g0(0, 2, rccw),
        g0(2, 2, flip), g1(2, 2, flip), g1(2, 0, rcw), g0(2, 0, rcw)]
       
    #Create full Glider loop.
    gg = []
    ox, oy = 35, 23
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[0](ox + dd, oy - dd), gliders[1](ox + 8 + dd, oy - 7 - dd)]
    dd = loopm
    gg += [gliders[2](45 + dd, 4 - dd), gliders[3](37 + dd, -3 - dd)]

    ox, oy = 26 + loopm, -4 - loopm
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[4](ox - dd, oy + dd), gliders[5](ox - 8 - dd, oy + 7 + dd)]
    dd = loopm
    gg += [gliders[6](16, 15), gliders[7](24, 22)]
    parity = 2*((loopw + 1)*(0, 0) + (1, 1))

    def buildLM():
        ''' Build a complete LineMaker '''
        #Populate glider loop. (jj, ii) are bitmap coords
        gloop = pattern()
        for j in xrange(bmwidth):
            jj = (j - delta + bmwidth - 1) % bmwidth

            #Is there a pixel at this location?
            if bm[ii][jj] == parity[j]:               
                gloop += gg[j]  #Add glider to the loop

        #Only put LineMaker if glider loop isn't empty
        if len(gloop) > 0:
            (LMBlank + gloop).put(Lx, Ly, trans)

    #Assemble blank LineMaker
    LMBlank = linemaker(loopm)

    #Do upper LineMakers
    trans = identity
    for i in xrange(mid):
        ii = mid - (i + 1)
        io = mid + (i + 1)
        Lx, Ly = io * LMdx, ii * LMdy
        delta = LMsx * io
        buildLM()

    #Do lower LineMakers
    trans = flip_y
    for i in xrange(mid, bmheight):
        ii = i
        io = i + 2
        Lx, Ly = io * LMdx + pdx, ii * LMdy + 128
        delta = LMsx * io - 1
        buildLM()

    #Eaters facing south-east & north-east
    eaterSE = pattern('2o$bo$bobo$2b2o!')
    eaterNE = eaterSE(0, 10, flip_y)
    eY = 59
    eaters = (eaterNE(0, eY), eaterSE(0, eY))
   
    #Eater horizontal displacement. Must be odd
    #bmwidth muliplier determines number of copies visible 'outside' printer.
    eX = (bmheight + 1) * LMdx - (copies * bmwidth - 1) * pdx   
    eX = 1 + eX // 2 * 2    #Adjust parity
    all = pattern()
    for i in xrange(bmheight):
        all += eaters[i % (1 + LMsx % 2)](eX, i * LMdy)
    all.put()   

    #Centre view over bitmap output area
    golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
    #golly.setmag(-2)    #Aliasing effects happen at smaller scales than -2 :(
    golly.fit()

def main():
    #Vertical distance between pixels. Maybe get this from user. minimum = 16
    pdy = 16

    #Generate & display a dot matrix printer from current selection
    dmprinter(pdy, copies=1)

    #golly.setcursor(3)
    golly.setoption("hashing", True)
    golly.setbase(2)
    golly.setstep(6)

if __name__  ==  '__main__':
    main()   
User avatar
PM 2Ring
 
Posts: 152
Joined: March 26th, 2009, 11:18 am

Re: Golly scripts

Postby Eylrid » October 10th, 2009, 6:32 pm

PM 2Ring wrote:p30 printer: update

This version comments out the setcursor() call, which fails on recent versions of Golly. It will also allow you to recover the uncompressed version of the compressed output script: simply run the output script at the command line instead of in Golly & the script will be printed to the terminal, so you can redirect the output to a file if you want to modify it. Note that the bitmap pattern is stored in one big string at the top of the script. This might cause problems with some editors if the bitmap is large, but it doesn't seem to phase Golly - I just tested it with a 332 x 341 (= 113212 pixels) image, and everything worked fine.

bm10b.py
# Golly Python script. Written by PM 2Ring, March 2009. Updated August 2009.

''' Create a 'bitmap printer'.

    Print a bitmap in LWSSs, using the current selection as the bitmap source.
    Save output pattern as Python source code.
'''

import zlib, base64, golly
from glife import *

def get_bitmap():
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection, aborting.")

    #Get bitmap size
    w, h = selrect[2:]
    #Adjust width, so it's in the form of 4m, m>1
    w = (w + 3) // 4 * 4
    w = max(8, w)

    #Initialize empty bitmap
    row = w * ['0']
    bm = [row[:] for i in xrange(h)]

    #Populate bitmap with cell data
    u, v = selrect[:2]
    cells = golly.getcells(selrect)
    cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
    for x, y in cellsxy:
        bm[y][x] = '1'
       
    #Convert to CSV string
    return ','.join([''.join(row) for row in bm])

prog = """       
def linemaker(loopm):
    LM = pattern('''34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
$2o11bo2bo7b3o21b2o$13bobo9b2o21b2o$12bobo$12bo9bo$23b2o$22b2o6$47b3o$
24b2o20bo3bo$24b2o19bo5bo$46bo3bo$47b3o$47b3o5$49b3o$44bo3b2ob2o$42b2o
4b2ob2o$35b2o6b2o3b5o$35b2o10b2o3b2o7$45b2o5b2o$45bo6bo$46b3o4b3o$48bo
6bo''')
    LMTop = LM + pattern('2b2o$2bo$obo$2o', 32, 7)
    LMBase = pattern('''11bo$10bo$10b3o12$23b2o$23bobo$10bobo13bo$9bo2bo2b2o6bo2bo12b2o$8b2o5b
obo8bo11b3o$2o4b2o3bo3bo3bo3bobo9bob2o15bo$2o6b2o5b3ob2o2b2o10bo2bo8b
3o4bobo$9bo2bo3b2o17bob2o16bobo$10bobo25b3o2b2o2bo7bo2bo3b2o$39b2o2bo
3bo7bobo4b2o$44bo2bo6bobo$26bo17b2o8bo$24bobo$25b2o2$36bo$36bobo$36b2o
7$33bo7bo$32b4o5bobo$27b2o2bo2b2o8b2o4b2o$27b2o2b2o11b2o4b2o$16b2o6b2o
10bo7b2o$16b2o5b3o10bo4bobo$24b2o10bo4bo$27b2o$27b2o''', 14, 29)
    LMBase += LM(55, 42, flip)
    return LMTop(8 + loopm, -21 - loopm) + LMBase

def main(pdy, copies, title):
    LMsx, pdx = 5, 15
    LMdx, LMdy = -LMsx * pdx, pdy
    bm = [[int(j) for j in i] for i in bits.split(',')]
    golly.new(title)
    golly.setrule("B3/S23")
    bmheight, bmwidth = len(bm), len(bm[0])
    mid = (bmheight + 1) // 2
    loopw = (bmwidth - 8) // 4
    loopm = pdx * loopw
    g0, g1 = pattern('2bo$2o$b2o'), pattern('obo$2o$bo')
    gliders = [g0, g1, g1(0, 2, rccw), g0(0, 2, rccw),
        g0(2, 2, flip), g1(2, 2, flip), g1(2, 0, rcw), g0(2, 0, rcw)]
    gg = []
    ox, oy = 35, 23
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[0](ox + dd, oy - dd), gliders[1](ox + 8 + dd, oy - 7 - dd)]
    dd = loopm
    gg += [gliders[2](45 + dd, 4 - dd), gliders[3](37 + dd, -3 - dd)]
    ox, oy = 26 + loopm, -4 - loopm
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[4](ox - dd, oy + dd), gliders[5](ox - 8 - dd, oy + 7 + dd)]
    dd = loopm
    gg += [gliders[6](16, 15), gliders[7](24, 22)]
    parity = 2*((loopw + 1)*(0, 0) + (1, 1))
    def buildLM():
        gloop = pattern()
        for j in xrange(bmwidth):
            jj = (j - delta + bmwidth - 1) % bmwidth
            if bm[ii][jj] == parity[j]:               
                gloop += gg[j] 
        (LMBlank + gloop).put(Lx, Ly, trans)
   
    LMBlank = linemaker(loopm)
    trans = identity
    for i in xrange(mid):
        ii = mid - (i + 1)
        io = mid + (i + 1)
        Lx, Ly = io * LMdx, ii * LMdy
        delta = LMsx * io
        buildLM()
    trans = flip_y
    for i in xrange(mid, bmheight):
        ii = i
        io = i + 2
        Lx, Ly = io * LMdx + pdx, ii * LMdy + 128
        delta = LMsx * io - 1
        buildLM()
    eaterSE = pattern('2o$bo$bobo$2b2o')
    eaterNE = eaterSE(0, 10, flip_y)
    eY = 59
    eaters = (eaterNE(0, eY), eaterSE(0, eY))
    eX = (bmheight + 1) * LMdx - (copies * bmwidth - 1) * pdx   
    eX = 1 + eX // 2 * 2
    all = pattern()
    for i in xrange(bmheight):
        all += eaters[i % (1 + LMsx % 2)](eX, i * LMdy)
    all.put()   
    golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
    golly.fit()
    #golly.setcursor(3)
"""

if __name__  ==  '__main__':   
    title = golly.getname().split('.')[0] + '-printer'
    prog = "bits = '%s'\n%s" % (get_bitmap(), prog)
    exec compile(prog, '<string>', 'exec')
    golly.duplicate()
    golly.setoption("syncviews", False)
    main(16, 1, title)   

    #Save pattern as a Python program
    hdr = '#Golly Python script\nimport golly\nfrom glife import *\n'
    ftr = "main(16, 1, '%s')" % title 
    s = """#Golly Python script
import zlib,base64
s=zlib.decompress(base64.decodestring('''
%s'''))
try:import golly
except ImportError:print s
else:   
from glife import *
exec compile(s,'<string>','exec')
main(16, 1, '%s')
""" % (base64.encodestring(zlib.compress(hdr + prog + ftr)), title)   
    f = open(title + '.py', 'w')
    f.write(s)
    f.close()     


Here's the original "straight" version of the above script. It doesn't have the ability to output the pattern as a Python script. As well as being a lot easier to read than the above, especially if you read it in an editor with syntax highlighting, it also has comments, which (hopefully) explain what's going on. :)

bm10.py
# Golly Python script. Written by PM 2Ring, March 2009. Updated August 2009.

''' Create a 'bitmap printer'.

    Print a bitmap in LWSSs, using the current selection as the bitmap source.
'''

import golly
from glife import *

def get_bitmap():
    ''' Get current selection & turn it into a bitmap. '''
    selrect = golly.getselrect()
    if len(selrect) == 0: golly.exit("There is no selection, aborting.")

    #Get bitmap size
    w, h = selrect[2:]
    #Adjust width, so it's in the form of 4m, m>1
    w = (w + 3) // 4 * 4
    w = max(8, w)

    #Initialize empty bitmap
    row = w * [0]
    bm = [row[:] for i in xrange(h)]

    #Populate bitmap with cell data
    u, v = selrect[:2]
    cells = golly.getcells(selrect)
    cellsxy = [(cells[i] - u, cells[i+1] - v) for i in xrange(0, len(cells), 2)]
    for x, y in cellsxy:
        bm[y][x] = 1
    return bm

def linemaker(loopm):
    ''' Create an empty LineMaker '''
    LM = pattern('''x = 56, y = 43
34b2o$34bo$25b2o5bobo$24b3o5b2o$12bo8bob2o$12bobo6bo2bo$2o11bobo5bob2o
$2o11bo2bo7b3o21b2o$13bobo9b2o21b2o$12bobo$12bo9bo$23b2o$22b2o6$47b3o$
24b2o20bo3bo$24b2o19bo5bo$46bo3bo$47b3o$47b3o5$49b3o$44bo3b2ob2o$42b2o
4b2ob2o$35b2o6b2o3b5o$35b2o10b2o3b2o7$45b2o5b2o$45bo6bo$46b3o4b3o$48bo
6bo!''')
    LMTop = LM + pattern('2b2o$2bo$obo$2o!', 32, 7)
    LMBase = pattern('''x = 64, y = 47
11bo$10bo$10b3o12$23b2o$23bobo$10bobo13bo$9bo2bo2b2o6bo2bo12b2o$8b2o5b
obo8bo11b3o$2o4b2o3bo3bo3bo3bobo9bob2o15bo$2o6b2o5b3ob2o2b2o10bo2bo8b
3o4bobo$9bo2bo3b2o17bob2o16bobo$10bobo25b3o2b2o2bo7bo2bo3b2o$39b2o2bo
3bo7bobo4b2o$44bo2bo6bobo$26bo17b2o8bo$24bobo$25b2o2$36bo$36bobo$36b2o
7$33bo7bo$32b4o5bobo$27b2o2bo2b2o8b2o4b2o$27b2o2b2o11b2o4b2o$16b2o6b2o
10bo7b2o$16b2o5b3o10bo4bobo$24b2o10bo4bo$27b2o$27b2o!''', 14, 29)
    LMBase += LM(55, 42, flip)
    return LMTop(8 + loopm, -21 - loopm) + LMBase

def dmprinter(pdy, copies=1):
    ''' Generate & display a dot matrix printer for named bitmap pattern '''
    #Horizontal pixel separation between LineMakers. minimum = 5
    LMsx = 5

    #Horizontal distance between bitmap pixels. Constant, due to LineMaker period
    pdx = 15

    #Distance between LineMakers
    LMdx, LMdy = -LMsx * pdx, pdy

    #Get bitmap pattern from current selection.
    bm = get_bitmap()

    #Make printer in a new layer
    golly.duplicate()
    golly.setoption("syncviews", False)

    #Build new window title from old
    title = golly.getname().split('.')[0]
    title = '%s_Printer [%d] v0.10' % (title, pdy)
    golly.new(title)

    #Make sure we're using the standard Life generation rule
    golly.setrule("B3/S23")

    #Bitmap dimensions. Width MUST be of form 4m, for m >=1
    bmheight = len(bm)
    bmwidth = len(bm[0])
    mid = (bmheight + 1) // 2

    loopw = (bmwidth - 8) // 4
    loopm = pdx * loopw

    #Gliders, heading north-east
    g0 = pattern('2bo$2o$b2o!')
    g1 = pattern('obo$2o$bo!')
    gliders = [g0, g1, g1(0, 2, rccw), g0(0, 2, rccw),
        g0(2, 2, flip), g1(2, 2, flip), g1(2, 0, rcw), g0(2, 0, rcw)]
       
    #Create full Glider loop.
    gg = []
    ox, oy = 35, 23
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[0](ox + dd, oy - dd), gliders[1](ox + 8 + dd, oy - 7 - dd)]
    dd = loopm
    gg += [gliders[2](45 + dd, 4 - dd), gliders[3](37 + dd, -3 - dd)]

    ox, oy = 26 + loopm, -4 - loopm
    for j in xrange(loopw + 1):
        dd = pdx * j
        gg += [gliders[4](ox - dd, oy + dd), gliders[5](ox - 8 - dd, oy + 7 + dd)]
    dd = loopm
    gg += [gliders[6](16, 15), gliders[7](24, 22)]
    parity = 2*((loopw + 1)*(0, 0) + (1, 1))

    def buildLM():
        ''' Build a complete LineMaker '''
        #Populate glider loop. (jj, ii) are bitmap coords
        gloop = pattern()
        for j in xrange(bmwidth):
            jj = (j - delta + bmwidth - 1) % bmwidth

            #Is there a pixel at this location?
            if bm[ii][jj] == parity[j]:               
                gloop += gg[j]  #Add glider to the loop

        #Only put LineMaker if glider loop isn't empty
        if len(gloop) > 0:
            (LMBlank + gloop).put(Lx, Ly, trans)

    #Assemble blank LineMaker
    LMBlank = linemaker(loopm)

    #Do upper LineMakers
    trans = identity
    for i in xrange(mid):
        ii = mid - (i + 1)
        io = mid + (i + 1)
        Lx, Ly = io * LMdx, ii * LMdy
        delta = LMsx * io
        buildLM()

    #Do lower LineMakers
    trans = flip_y
    for i in xrange(mid, bmheight):
        ii = i
        io = i + 2
        Lx, Ly = io * LMdx + pdx, ii * LMdy + 128
        delta = LMsx * io - 1
        buildLM()

    #Eaters facing south-east & north-east
    eaterSE = pattern('2o$bo$bobo$2b2o!')
    eaterNE = eaterSE(0, 10, flip_y)
    eY = 59
    eaters = (eaterNE(0, eY), eaterSE(0, eY))
   
    #Eater horizontal displacement. Must be odd
    #bmwidth muliplier determines number of copies visible 'outside' printer.
    eX = (bmheight + 1) * LMdx - (copies * bmwidth - 1) * pdx   
    eX = 1 + eX // 2 * 2    #Adjust parity
    all = pattern()
    for i in xrange(bmheight):
        all += eaters[i % (1 + LMsx % 2)](eX, i * LMdy)
    all.put()   

    #Centre view over bitmap output area
    golly.setpos(str(-pdx * bmwidth // 2 + (bmheight - 1) * LMdx), str(Ly//2))
    #golly.setmag(-2)    #Aliasing effects happen at smaller scales than -2 :(
    golly.fit()

def main():
    #Vertical distance between pixels. Maybe get this from user. minimum = 16
    pdy = 16

    #Generate & display a dot matrix printer from current selection
    dmprinter(pdy, copies=1)

    #golly.setcursor(3)
    golly.setoption("hashing", True)
    golly.setbase(2)
    golly.setstep(6)

if __name__  ==  '__main__':
    main()   


I can't seem to get it to work for some reason.
Eylrid
 
Posts: 30
Joined: October 8th, 2009, 10:28 pm

Next

Return to Scripts

Who is online

Users browsing this forum: No registered users and 1 guest