Last active
October 13, 2018 19:26
-
-
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]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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