Tic Tac Toe

from fasthtml.common import *

style = Style("""body{
    min-height: 100vh;
    margin:0;
    background-color: #1A1A1E;
    display:grid;
}""") # custom style to be applied globally.

hdrs = (Script(src="https://cdn.tailwindcss.com") ,
        Link(rel="stylesheet", href="/files/examples/applications/tic_tac_toe/output.css"))

app, rt = fast_app(hdrs=(hdrs, style), pico=False)

current_state_index = -1 #Used to navigate the current snapshot of the board
button_states = [[None for _ in range(9)] for _ in range(9)] #2D array to store snapshots of board
win_states = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
] #possible win streaks/states for Xs and Os

winner_found_game_ended = False

def check_win(player) -> bool:
    global button_states, current_state_index, winner_found_game_ended
    """Checks if there's a win streak present in the board. Uses the win states list to check
       If text at all text indices are equal and its not the placeholder text ("."), change the global variable "winner_found_game_ended" to True"""
    for cell_1, cell_2, cell_3 in win_states:
        if (
            button_states[current_state_index][cell_1] != None
            and button_states[current_state_index][cell_1]
            == button_states[current_state_index][cell_2]
            and button_states[current_state_index][cell_2]
            == button_states[current_state_index][cell_3]):
            winner_found_game_ended = True
            return f"Player {player} wins the game!"

    if all(value is not None for value in button_states[current_state_index]):
        #if the current snapshot of the board doesn't have any placeholder text and there is no winning streak
        winner_found_game_ended = True
        return "No Winner :("

    #will keep returning this value [because its called after every button click], until a winner or none is found
    return f'''Player {'X' if player == 'O' else 'O'}'s turn!'''


def handle_click(index: int):
    """This function handles what text gets sent to the button's face depending on whose turn it is uses a weird algorithm"""
    global button_states, current_state_index
    next_index = current_state_index + 1
    button_states[next_index] = button_states[current_state_index][:] #make a copy of the current snapshot to add to the next snapshot

    if button_states[current_state_index][index] is None:
        if "X" not in button_states[current_state_index] or button_states[
            current_state_index
        ].count("X") <= button_states[current_state_index].count("O"):
            button_states[next_index][index] = "X"
        else:
            button_states[next_index][index] = "O"
    current_state_index += 1
    return button_states[next_index][index]


@app.get("/on_click")  # On click, call helper function to alternate between X and O
def render_button(index:int):
    global button_states, current_state_index

    player = handle_click(index)
    winner = check_win(player)  # function that checks if there's a winner

    buttons = [Button(
            f'''{text if text is not None else '.' }''',
            cls="tic-button-disabled" if (text is not None) or winner_found_game_ended else "tic-button",
            disabled=True if (text is not None) or winner_found_game_ended else False,
            hx_get=f"on_click?index={idx}",
            hx_target=".buttons-div", hx_swap='outerHTML')
        for idx, text in enumerate(button_states[current_state_index])
    ]
    """rerenders buttons based on the next snapshot.
       I initially made this to render only the button that gets clicked.
       But to be able to check the winner and stop the game, I have to use the next snapshot instead
       if you wanna see the previous implementation, it should be in one of the commits."""
    board = Div(
                Div(winner, cls="justify-self-center"),
                Div(*buttons, cls="grid grid-cols-3 grid-rows-3"),
                cls="buttons-div font-bevan text-white font-light grid justify-center")
    return board


# Rerenders the board if the restart button is clicked.
# Also responsible for initial rendering of board when webpage is reloaded
@app.get("/restart")
def render_board():
    global button_states, current_state_index, winner_found_game_ended

    current_state_index = -1
    button_states = [[None for _ in range(9)] for _ in range(9)]
    winner_found_game_ended = False

    # button component
    buttons = [
        Button(
            ".",
            cls="tic-button",
            hx_get=f"on_click?index={i}",
            hx_swap="outerHTML", hx_target=".buttons-div")
        for i, _ in enumerate(button_states[current_state_index])
    ]
    return  Div(Div("Player X starts the game",cls="font-bevan text-white justify-self-center"),
                Div(*buttons, cls="grid grid-cols-3 grid-rows-3"),
                cls="buttons-div grid")


@app.get("/")
def homepage():
    global button_states
    return Div(
        Div(
            H1("Tic Tac Toe!", cls="font-bevan text-5xl text-white"),
            P("A FastHTML app by Adedara Adeloro", cls="font-bevan text-custom-blue font-light"),
            cls="m-14"),
        Div(
            render_board.__wrapped__(),  # render buttons.
            Div(
                Button(
                    "Restart!",
                    disabled=False,
                    cls="restart-button",
                    hx_get="restart", hx_target=".buttons-div", hx_swap="outerHTML"),
                cls="flex flex-col items-center justify-center m-10"),
            cls="flex flex-col items-center justify-center"),
        cls="justify-center items-center min-h-screen bg-custom-background")