i am trying to make a chess game where an LLM is the opponent. i make a move, then the move and board info is sent to LLM context. the llm replies and generally the first word in the reply is the move, so i grab that word and try to enter it as a move for the LLM. separate from the game itself, i added a chat area. the chat area should not interfere with the game. really, the only benefit of the chat area during the game is it allows me to see the move the LLM is making. while not making moves in the game, i'd like to be able to chat with the LLM if i want. currently the only problem i see is that when I make a move, the LLM responds twice and i don't know why, but it's something with the way the code is setup.

import tkinter as tk
from PIL import Image, ImageTk
import chess
import requests
import re

# Functions
def draw_board(board, selected_square=None):
    global board_images
    board_images = {}
    canvas.delete("all")
    canvas_width = 900
    canvas_height = 500
    canvas.config(width=canvas_width, height=canvas_height)
    start_x = 2
    start_y = 2

    for square in chess.SQUARES:
        rank = chess.square_rank(square)
        file = chess.square_file(square)
        x0 = start_x + file * square_size
        y0 = start_y + (7 - rank) * square_size
        if (rank + file) % 2 == 0:
            square_color = "white"
        else:
            square_color = "light gray"
        canvas.create_rectangle(x0, y0, x0 + square_size, y0 + square_size, fill=square_color)

        # Check if the square is selected and draw a highlight
        if square == selected_square:
            canvas.create_rectangle(x0, y0, x0 + square_size, y0 + square_size, fill="yellow")

        piece = board.piece_at(square)
        if piece:
            image = Image.open(PIECE_IMAGES[str(piece)]).resize((square_size, square_size), Image.Resampling.LANCZOS)
            photo = ImageTk.PhotoImage(image)
            board_images[square] = photo
            canvas.create_image(x0 + square_size // 2, y0 + square_size // 2, image=photo)

def process_move(move, game_board):
    global san_moves, sa_number, total_moves, history, user_turn, chat_window, valid_moves, llm_turn

    try:
        if llm_turn:
            result_label.config(text="It's not your turn to move.")
            return

        if move in game_board.legal_moves:
            game_board.push(move)
            fen_notation = game_board.fen()
            shredder_fen = chess.Board(fen_notation)
            result_label.config(text=f"FEN Notation: {fen_notation}")
            draw_board(game_board)

            if total_moves % 2 == 1:
                sa_number += 1
                san_moves += f"{sa_number}. {move} "
                total_moves += 1
                history = []
                move_list = re.findall(r'\((.*?)\)', str(valid_moves))
                chat_window.delete(1.0, tk.END)
                role = "user"
            else:
                san_moves += f"{move} "
                total_moves += 1

            print("SAN Moves:", san_moves)

            if move_list == ['']:
                message = "Checkmate."
                chat_window.insert(tk.END, f"\nCheckmate")
            else:
                message = f"Pick a move from THIS list {move_list}.\nThe board is currently at:\n{shredder_fen}."

            history.append({"role": role, "content": message})
            interact_with_llm(message)
            llm_turn = True  # Set the LLM turn to True after user move
        else:
            result_label.config(text="Illegal move")
            return  # Return here to avoid sending message to LLM for an illegal move
    except ValueError:
        result_label.config(text="Invalid move")

def interact_with_llm(message):
    global history, llm_turn

    try:
        data = {
            "mode": "chat",
            "character": "ChessAI",
            "seed": -1,
            "messages": history
        }

        response = requests.post(api_url, headers=headers, json=data).json()

        if 'choices' in response and response['choices']:
            assistant_message = response['choices'][0]['message']['content']
            history.append({"role": "assistant", "content": assistant_message})
            chat_window.insert(tk.END, f"\nAssistant: {assistant_message}\n")
            print_history()

            llm_turn = False  # Reset llm_turn to False after LLM responds
    except Exception as e:
        print(f"Error in LLM request: {e}")
        chat_window.insert(tk.END, "\nAssistant: Error in LLM request")
        llm_turn = False  # Reset llm_turn even in case of an error

def on_square_click(event):
    global selected_square, is_second_click

    file = (event.x + 20) // square_size
    rank = 7 - (event.y + 10) // square_size
    square = chess.square(file, rank)

    if not is_second_click:
        # First click: Select the piece
        move_entry.delete(0, tk.END)
        move_entry.insert(tk.END, chess.square_name(square))
        selected_square = square
        is_second_click = True
        draw_board(game_board, selected_square)
    else:
        # Second click: Generate and process the move
        move = chess.Move(selected_square, square)

        try:
            if move in game_board.legal_moves:
                process_move(move, game_board)
                is_second_click = False

                # Pass move information to LLM
                message = f"User made move: {move}.\nCurrent board state:\n{game_board.fen()}"
                interact_with_llm(message)

            else:
                result_label.config(text="Illegal move")
                draw_board(game_board, selected_square)
                is_second_click = False
        except ValueError:
            result_label.config(text="Invalid move")
            is_second_click = False

def update_game_board(move_str):
    global san_moves, sa_number, total_moves, history, user_turn, chat_window, valid_moves

    try:
        game_move = game_board.parse_san(move_str)
        if game_move in game_board.legal_moves:
            game_board.push(game_move)
            fen_notation = game_board.fen()
            shredder_fen = chess.Board(fen_notation)
            result_label.config(text=f"FEN Notation: {fen_notation}")

            # Add the valid move to the SAN moves string with move number
            if total_moves % 2 == 1:
                san_moves += f"{sa_number}. {move_str} "
                total_moves += 1
                history = []
                move_list = re.findall(r'\((.*?)\)', str(valid_moves))
                chat_window.delete(1.0, tk.END)  # Clear the chat window
                role = "user"
                if move_list == ['']:
                    message = "Checkmate."
                    chat_window.insert(tk.END, f"\nCheckmate")
                else:
                    message = f"Pick a move from THIS list {move_list}.\nThe board is currently at:\n{shredder_fen}."
                    history.append({"role": role, "content": message})
                    interact_with_llm(message)

            else:
                san_moves += f"{move_str} "
                total_moves += 1
                sa_number += 1

            # Print or use san_moves as needed
            print("SAN Moves:", san_moves)

            draw_board(game_board)
        else:
            result_label.config(text="Illegal move")
    except ValueError:
        result_label.config(text="Invalid move")

def submit_move():
    global valid_moves, move
    move_str = move_entry.get().strip()

    if move_str:
        try:
            update_game_board(move_str)
            # Extract moves using regular expression
            moves_list = re.findall(r'\((.*?)\)', str(valid_moves))
            if moves_list:
                print(moves_list[0])
        except ValueError:
            try:
                game_board.set_fen(move_str)
                draw_board(game_board)
                result_label.config(text=f"FEN Notation: {move_str}")
            except ValueError:
                result_label.config(text="Invalid move")
    move_entry.delete(0, tk.END)

def submit_message():
    global chat_window, user_color, history
    message = input_area.get("1.0", tk.END).strip()
    role = "user"
    chat_window.insert(tk.END, f"\n{role.capitalize()}: {message}\n")
    history.append({"role": role, "content": message})
    interact_with_llm(message)

def on_enter(event):
    # Check if the cursor is in the input area
    if event.widget == input_area:
        submit_message()
    elif event.widget == move_entry:
        submit_move()  # Assuming you want to submit the move in this case

def print_history():
    print("\n=== Updated History ===")
    for entry in history:
        print(f"{entry['role']}: {entry['content']}")
    print("=======================")

def clear_history():
    global history, chat_window, game_board, san_moves
    history = []
    chat_window.delete(1.0, tk.END)  # Clear the chat window
    game_board = chess.Board(initial_fen)
    draw_board(game_board)
    san_moves = ""

def choose_white():
    global user_color, white_button, black_button
    user_color = "white"
    history.append({"role": "user", "content": "White"})
    print_history()
    white_button.destroy()  # Remove the "Choose White" button
    black_button.destroy()  # Remove the "Choose Black" button

def choose_black():
    global user_color, white_button, black_button
    user_color = "black"
    history.append({"role": "user", "content": "Black"})
    print_history()
    white_button.destroy()  # Remove the "Choose White" button
    black_button.destroy()  # Remove the "Choose Black" button

# Global Variables
user_color = None
history = []
san_moves = ""  # Initialize an empty string to store SAN moves
sa_number = 1  # Initialize real number of moves
total_moves = 1
llm_move = ""
llm_turn = False
api_url = 'http://127.0.0.1:5000/v1/chat/completions'
headers = {"Content-Type": "application/json"}
initial_fen = chess.STARTING_FEN
game_board = chess.Board(initial_fen)
valid_moves = game_board.legal_moves
board_images = {}
square_size = 60  # Size of a square in the board
selected_square = None
move = None
is_second_click = False
# Create a dictionary to map piece symbols to image filenames
PIECE_IMAGES = {
    "P": "images/Wp.png",
    "p": "images/bp.png",
    "R": "images/WR.png",
    "r": "images/bR.png",
    "N": "images/WN.png",
    "n": "images/bN.png",
    "B": "images/WB.png",
    "b": "images/bB.png",
    "Q": "images/WQ.png",
    "q": "images/bQ.png",
    "K": "images/WK.png",
    "k": "images/bK.png"
}

# Tkinter Window
window = tk.Tk()
window.title("Chess")

# Tkinter Components
canvas = tk.Canvas(window)
canvas.pack()
draw_board(game_board)
canvas.bind("<Button-1>", lambda event: on_square_click(event))
move_entry = tk.Entry(window)
move_entry.place(x=7, y=520)
move_entry.bind("<Return>", on_enter)
submit_btn = tk.Button(window, text="Make Move", command=submit_move)
submit_btn.place(x=185, y=518)
result_label = tk.Label(window, text="FEN Notation: " + game_board.fen())
result_label.place(x=7, y=495)
chat_window = tk.Text(window, height=20, width=45, wrap=tk.WORD)
chat_window.place(x=515, y=20)
input_area = tk.Text(window, height=5, width=45, wrap=tk.WORD)
input_area.place(x=515, y=400)
input_area.bind("<Return>", on_enter)
submit_btn = tk.Button(window, text="Send", command=submit_message)
submit_btn.pack(side=tk.RIGHT, padx=10, pady=10)
clear_history_btn = tk.Button(window, text="Clear History", command=clear_history)
clear_history_btn.place(x=300, y=518)
white_button = tk.Button(window, text="Choose White", command=choose_white)
white_button.place(x=185, y=200)
black_button = tk.Button(window, text="Choose Black", command=choose_black)
black_button.place(x=185, y=240)

window.mainloop()