Cellular Automata

from fasthtml.common import *
from starlette.responses import Response
from uuid import uuid4


generator = {}
bindict = {
    (1,1,1):0,
    (1,1,0):1,
    (1,0,1):2,
    (1,0,0):3,
    (0,1,1):4,
    (0,1,0):5,
    (0,0,1):6,
    (0,0,0):7}
initial_row = [0]*50 + [1] + [0]*50
color_map = {0:"white", 1:"black"}

####################
### HTML Widgets ###
####################

explanation = Div(
    H1("Cellular Automata"),
    H4("Input explanations:"),
    Ul(Li(Strong("Rule: "),"Determines the next state of a cell based on the current state of the cell and its neighbors."),
        Li(Strong("Generations: "),"Determines how many generations to run the automaton."),
        Li(Strong("Width: "),"Determines the width of the grid."),))

def progress_bar(percent_complete: float):
    return Div(hx_swap_oob="innerHTML:#progress_bar")(
            Progress(value=percent_complete))

def mk_box(color,size=5):
    return Div(cls="box", style=f"background-color:{color_map[color]};height:{size}px;width:{size}px;margin:0;display:inline-block;")

def mk_row(colors,font_size=0,size=5):
    return Div(*[mk_box(color,size) for color in colors], cls="row",style=f"font-size:{font_size}px;")

def mk_button(show):
    return Button("Hide Rule" if show else "Show Rule",
        hx_get="show_rule?show=" + ("False" if show else "True"),
        hx_target="#rule", id="sh_rule", hx_swap_oob="outerHTML",
        hx_include="[name='rule_number']")

########################
### FastHTML Section ###
########################

app, rt = fast_app()


@rt
def index(sess):
    if 'id' not in sess: sess['id'] = str(uuid4())
    return Title("Cellular Automata"),Main(Div(
        Div(P(explanation,id="explanations")),
        Form(Group(
            Div(hx_target='this', hx_swap='outerHTML')(Label(_for="rule_number", cls="form-label")("Rule"),
                Input(type='number', name="rule_number", id='rule_set', value="30",hx_post='validate/rule_number')),
            Div(hx_target='this', hx_swap='outerHTML')(Label("Generations", cls="form-label"),
                Input(type='number',name="generations", id='generations_set',  value="50",hx_post='validate/generations', hx_indicator='#generationsind')),
            Div(hx_target='this', hx_swap='outerHTML')(Label("Width", cls="form-label"),
                Input(type='number',name="width", id='width_set',  value="100", hx_post='validate/width', hx_indicator='#widthind')), 
            Button(cls="btn btn-active btn-primary", type="submit", hx_get="run", 
                   hx_target="#grid", hx_include="[name='rule_number'],[name='generations'],[name='width']", hx_swap="outerHTML")("Run"))),
        Group(
            Div(style="margin-left:50px")(
                Div(id="progress_bar"),
                Div(id="grid")),
            Div(style="margin-right:50px; max-width:200px")(
                    mk_button(False),
                    Div(id="rule"),
                    ))))

@rt('/show_rule')
def get(rule_number: int, show: bool):
    rule = [int(x) for x in f'{rule_number:08b}']
    return Div(
        Div(mk_button(show)),
        Div(*[Group(
            Div(mk_row(list(k),font_size=10,size=20),style="max-width:100px"), 
            Div(P(" -> "),style="max-width:100px"), 
            Div(mk_box(rule[v],size=20),style="max-width:100px")) for k,v in bindict.items()] if show else '')
    )

@rt('/run')
def get(rule_number: int, generations: int, width: int, sess):

    errors = {'rule_number': validate_rule_number(rule_number),
              'generations': validate_generations(generations),
              'width': validate_width(width)}
    # Removes the None values from the errors dictionary (No errors)
    errors = {k: v for k, v in errors.items() if v is not None}
    # Return Button with error message if they exist
    
    if errors:
        return Div(Div(id="grid"),
                   Div(id="progress_bar",hx_swap_oob="outerHTML:#progress_bar"),
                Div(id='submit-btn-container',hx_swap_oob="outerHTML:#submit-btn-container")(
                    Button(cls="btn btn-active btn-primary", type="submit", 
                           hx_get="run", hx_target="#grid", 
                           hx_include="[name='rule_number'],[name='generations'],[name='width']", hx_swap="outerHTML")("Run"),
                    *[Div(error, style='color: red;') for error in errors.values()]))

    start = [0]*(width//2) + [1] + [0]*(width//2)
    global generator 
    generator[sess['id']] = run(rule=rule_number,generations=generations,start=start)
    return Div(
        Div(style=f"width: {(width+1)*5}px",id="progress_bar",hx_swap_oob="outerHTML:#progress_bar"),
        Div(id="next",hx_trigger="every .1s", hx_get="next", hx_target="#grid",hx_swap="beforeend"),id="grid")

@rt('/next')
def get(sess):
    global generator
    g,val = next(generator[sess['id']],(False,False))
    if val: return Div(
        progress_bar(g),
        mk_row(val))
    else: 
        del generator[sess['id']]
        return Response(status_code=286)

@rt('/validate/rule_number')
def post(rule_number: int): return inputTemplate('Rule Number', 'rule_number',rule_number, validate_rule_number(rule_number))

@rt('/validate/generations')
def post(generations: int): return inputTemplate('Generations', 'generations', generations, validate_generations(generations))

@rt('/validate/width')
def post(width: int): return inputTemplate('Width', 'width', width, validate_width(width))

#########################
### Application Logic ###
#########################

def run(rule=30, start = initial_row, generations = 100):
    rule = [int(x) for x in f'{rule:08b}']
    yield 0, start
    old_row = [0] + start + [0]
    new_row = []
    for g in range(1,generations):
        for i in range(1,len(old_row)-1):
            key=tuple(old_row[i-1:i+2])
            new_row.append(rule[bindict[key]])
        yield (g+1)/generations,new_row
        old_row = [0] + new_row + [0]
        new_row = []

########################
### Validation Logic ###
########################

def validate_rule_number(rule_number: int):
    if (rule_number < 0) or (rule_number > 255 ): return "Enter an integer between 0 and 255 (inclusive)"

def validate_generations(generations: int):
    if generations < 0: return "Enter a positive integer"
    if generations > 200: return "Enter a number less than 200"

def validate_width(width: int):
    if width < 0: return "Enter a positive integer"
    if width > 200: return "Enter a number less than 200"

def inputTemplate(label, name, val, errorMsg=None, input_type='number'):
    # Generic template for replacing the input field and showing the validation message
    return Div(hx_target='this', hx_swap='outerHTML', cls=f"{errorMsg if errorMsg else 'Valid'}")(
               Label(label), # Creates label for the input field
               Input(name=name,type=input_type,value=f'{val}',style="width: 340px;",hx_post=f'validate/{name.lower()}'), # Creates input field
               Div(f'{errorMsg}', style='color: red;') if errorMsg else None) # Creates red error message below if there is an error