Skip to content

Instantly share code, notes, and snippets.

@supposedly
Last active October 13, 2018 19:26
Show Gist options
  • Save supposedly/9dacf642a7d21517a8cbcd332107120d to your computer and use it in GitHub Desktop.
Save supposedly/9dacf642a7d21517a8cbcd332107120d to your computer and use it in GitHub Desktop.
Usage: `python ext_generations.py [output directory] [rulestring] [OPTIONAL rulename for the output file]
import sys
from os import path
ACTIVE, INACTIVE = 'active', 'inactive' # For convenience/ease of renaming the variables, should need be
# TODO: Consider reordering alphabetically to match Golly
N_HOODS = 'cekainyqjrtwz'
NAPKINS = {
k: dict(zip(N_HOODS, ([INACTIVE if state == '0' else ACTIVE for state in npk] for npk in v))) for k, v in {
'0': '',
'1': ('00000001', '10000000'),
'2': ('01000001', '10000010', '00100001', '10000001', '10001000', '00010001'),
'3': ('01000101', '10100010', '00101001', '10000011', '11000001', '01100001', '01001001', '10010001', '10100001', '10001001'),
'4': ('01010101', '10101010', '01001011', '11100001', '01100011', '11000101', '01100101', '10010011', '10101001', '10100011', '11001001', '10110001', '10011001'),
'5': ('10111010', '01011101', '11010110', '01111100', '00111110', '10011110', '10110110', '01101110', '01011110', '01110110'),
'6': ('10111110', '01111101', '11011110', '01111110', '01110111', '11101110'),
'7': ('11111110', '01111111'),
'8': ''
}.items()
}
def normalize_nontot(sz, segment):
"""
Consistently-order series of nontot-notation letters, and expand
negated/empty series into their respective non-shorthand forms
"""
if not segment or segment[0] == '-':
return [t for t in N_HOODS[:sz] if t not in segment]
return [t for t in N_HOODS if t in segment]
def combine_rstring(segment):
"""
Rulestring to dict:
{cellstate number: [individual non-totalistic cell configurations]}
"""
ret = {}
for i, total in enumerate(segment, 1):
if total.isdigit():
after = next((idx for idx, j in enumerate(segment[i:], i) if j.isdigit()), len(segment))
ret[total] = normalize_nontot(len(NAPKINS[total]), segment[i:after])
return ret
def normalize_rstring(segment):
"""
Ensure autogenerated rulenames are consistent by enforcing consistent
nontot letter ordering
"""
ret = []
for i, total in enumerate(segment, 1):
if total.isdigit():
seg = segment[i:next((idx for idx, j in enumerate(segment[i:], i) if j.isdigit()), len(segment))]
ret.append(total)
if seg:
inverse = seg[0] == '-'
ret.append(('_' if inverse else '') + ''.join(normalize_nontot(len(NAPKINS[total]), seg[inverse:])))
return ''.join(ret)
def unbind_vars(transition, start=0):
"""
Suffix varnames in transition with locally-unique numbers so that Golly
doesn't bind the identical names
"""
ret, seen = [], {}
for state in transition:
if isinstance(state, int) or state.isdigit():
ret.append(state)
else:
seen[state] = cur = seen.get(state, -1) + 1
ret.append('{}_{}'.format(state, cur))
return ret
def _lazy_tr(states):
"""
Take sequence of 2- or 3-tuples, interpreting them as
(
cellstate value to repeat+unbind,
number of times to repeat,
optional value to start bindings at,
)
and produce an expanded/chained form thereof
"""
for value in states:
if isinstance(value, tuple):
state, count, *start_at = value
yield from unbind_vars([state] * count, *start_at)
else:
yield value
def tr(*states):
"""Generate a transition (as list) from varargs"""
return list(_lazy_tr(states))
def make_totalistic(birth, survival):
transitions = []
# Birth
transitions.extend(
tr(0, (ACTIVE, n), (INACTIVE, 8 - n), 1)
for n in map(int, birth)
)
# Survival
transitions.extend(
tr((ACTIVE, n + 1), (INACTIVE, 8 - n), ACTIVE+'_0')
for n in map(int, survival)
)
return 'permute', transitions
def make_nontot(birth, survival):
transitions = []
birth, survival = combine_rstring(birth), combine_rstring(survival)
# Birth
transitions.extend(
unbind_vars((0, *NAPKINS[total][configuration], 1))
for total, configurations in birth.items()
for configuration in configurations
)
# Survival
transitions.extend(
[*unbind_vars((ACTIVE, *NAPKINS[total][configuration])), ACTIVE+'_0']
for total, configurations in survival.items()
for configuration in configurations
)
return 'rotate4reflect', transitions
def make_rule(birth, survival, age_pattern):
active, inactive = [], [0]
n_states = 1 + sum(age_pattern)
tr_func = make_totalistic if (birth + survival).isdigit() else make_nontot
if n_states > 255:
raise SystemExit(
"ERROR: You've defined too many dying states -- {} total, "
'but the maximum allowed by Golly is 255.'.format(n_states)
)
symmetry_type, transitions = tr_func(birth, survival)
# Transition toward death where unspecified
transitions.extend(
tr(state, ('all', 8), (state + 1) % n_states)
for state in range(1, n_states) # state 0 isn't included
)
lower = 1 # state 0 is always 'inactive', so we start at 1
for idx, upper in enumerate(age_pattern):
(inactive if idx % 2 else active).extend(range(lower, lower+upper))
lower += upper
return symmetry_type, n_states, active, inactive, transitions
def write_table(fp, rulename, symmetries, n_states, active, inactive, transitions):
fp.write('@RULE {}\n@TABLE\n'.format(rulename))
fp.write('n_states:{}\nneighborhood:Moore\nsymmetries:{}\n'.format(n_states, symmetries))
# Variables
def define_var(name, var):
if not var:
raise SystemExit(
'ERROR: Var for {!r} states is coming up empty. '
'Are you sure this is a valid rule?'.format(name)
)
fp.write('\nvar {0}_0={1}\n'.format(name, set(var)))
for n in range(8):
fp.write('var {0}_{1}={0}_0\n'.format(name, n + 1))
define_var(ACTIVE, active)
define_var(INACTIVE, inactive)
define_var('all', range(n_states))
# Transitions
for tr in transitions:
fp.write('\n' + ','.join(map(str, tr)))
def parse_rstring(rstring, rname):
"""
"Officially"-supported formats:
2-i34q/3/x-x-x-x (standard form)
B3,S2_i34q,ax_ix_ax_ix (standard 'rulename-&-filename-safe' form)
2-i34q/3/Dx-x-x-x
B3/S2-i34q/x-x-x-x
B3/S2-i34q/Dx-x-x-x
...where all capital letters are case-insensitive, x stands for any
digit or series of digits, and all other characters are literal.
"""
survival, birth, age_pattern = map(str.lower, rstring.split(',/'['/' in rstring]))
age_pattern = age_pattern.translate(str.maketrans('_', '-', 'dai'))
if survival.startswith('b'):
birth, survival = survival.replace('_', '-'), birth.replace('_', '-')
rule = *_, age_pattern = birth.lstrip('b'), survival.lstrip('s'), tuple(map(int, age_pattern.lstrip('a').split('-')))
rname = rname[0] if rname else 'B{},S{},{}'.format(
*(segment if segment.isdigit() else normalize_rstring(segment) for segment in rule[:2]),
'_'.join('{1}{0}'.format(name, 'i' if idx % 2 else 'a') for idx, name in enumerate(age_pattern))
)
return rname, rule
if __name__ == '__main__':
outdir, rulestring, *rulename = sys.argv[1:]
rulename, rule_info = parse_rstring(rulestring, rulename)
with open(path.join(outdir, rulename + '.rule'), 'w') as fp:
write_table(fp, rulename, *make_rule(*rule_info))
print('Created {}'.format(path.realpath(fp.name)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment