#! /usr/bin/env python3

"""Take an initial row and a cellular automaton rule and run the
automaton for a given number of iterations"""

import random

def main():
    n_steps = 2000
    n_cells = 550
    row = set_first_row_random(n_cells)
    # row = set_first_row_specific_points(n_cells, [270])
    # row = set_first_row_specific_points(n_cells, [200, 380])
    print_row(row)

    # here are some pre-defined special rules that give rather attractive results
    ##
    # rule = '01101000'           # the basic rule
    # rule = '00011110'           # the famous rule 30
    rule = '01101110'           # the famous rule 110

    for i in range(n_steps):
        row = take_step(rule, row)
        print_row(row)

def take_step(rule, row):
    """a single iteration of the cellular automaton"""
    n_cells = len(row)
    new_row = [0]*n_cells
    for i in range(n_cells):
        neighbors = [row[(i - 1 + n_cells) % n_cells], row[i], row[(i + 1) % n_cells]]
        # new_row[i] = new_cell(neighbors)
        ## NOTE: new_cell_with_rule() is part of the extended code (at
        ## the bottom)
        new_row[i] = new_cell_with_rule(rule, neighbors)
    return new_row

def new_cell(neighbors):
    """looks at the neighborhood of three cells and determine what the
    successor of the central cell should be"""
    ## this simple approach decides on the next cell based on the sum
    ## of the neighbors -- if both neighbors are active we are
    ## overcrowded and we die; if one is active then we come to life;
    ## if none are active we starve and die.
    if neighbors[0] + neighbors[2] == 2: # try [0] and [1] for a different pattern
        new_cell = 0
    elif neighbors[0] + neighbors[2] == 1:
        new_cell = 1
    else:
        new_cell = 0
    return new_cell

def set_first_row_random(n_cells):
    """sets the first row to random values"""
    row = [0]*n_cells
    for i in range(n_cells):
        row[i] = random.randint(0, 1)
    return row

def set_first_row_specific_points(n_cells, active_pts):
    """takes a list of specific cells to be activated in the first row"""
    row = [0]*n_cells
    for pt in active_pts:       # only activate the given cells
        print(pt)
        row[pt] = 1
    return row

def print_row(row):
    """prints a row, represented as a blank if the cell is 0 or a special
    symbol (like a period) if it's 1"""
    on_marker = 'x'
    row_str = ''
    for cell in row:
        if cell:
            symbol = on_marker
        else:
            symbol = ' '
        print(symbol, end="")
    print()

## NOTE: new_cell_with_rule() is extended code; you can skip it on a
## first implementation
def new_cell_with_rule(rule, neighbors):
    """Applies a rule encoded as a binary string -- since a neighborhood
    of 3 binary cells can have 8 possible patterns, it's a string of 8
    bits.  You can modify it to be any of the 256 possible strings of
    8 bits.  I provide a couple of examples, and you can try many others."""
    if not rule:
        rule = '01101000'       # the default rule
    rule_index = neighbors[0] + 2*neighbors[1] + 4*neighbors[2]
    cell = int(rule[rule_index])
    return cell

if __name__ == '__main__':
    main()
