Back to Home

Chess with LLM on Python without hallucinations

The article describes the implementation of a chess AI based on LLM using python-chess for validation. FEN/PGN/UCI are used in prompts and fallback for stability. Free access via OpenRouter.

Create a chess AI on Llama without errors
Advertisement 728x90

Building a Chess AI with LLM and Python — No Cost, No Hallucinations

Developers often need to integrate LLMs into game apps, but OpenAI's API can be expensive. An affordable alternative is OpenRouter, which offers free open-source models tagged :free. This lets you run queries to Llama 4 at no cost, with message volume limits.

Client initialization uses the standard openai interface:

class LLMAI:
    def __init__(self, model_name="meta-llama/llama-4-maverick-17b-128e-instruct:free"):
        self.client = openai.OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=OPENROUTER_API_KEY,
        )
        self.model_name = model_name
        self.move_count = 0

Setting base_url ensures compatibility with existing code. The model receives a prompt for each move, minimizing computational load.

Google AdInline article slot

Passing Board State to the Prompt

LLMs don’t process visual boards, so context must be built in text form. Standard FEN and PGN formats are enhanced with a list of legal moves from python-chess to prevent hallucinations.

Context-building code:

# Get current position in FEN format
fen = board.fen()

# Get move history in PGN format
pgn_moves = []
temp_board = chess.Board()

# History of moves
for move in board.move_stack:
    pgn_moves.append(temp_board.san(move))
    temp_board.push(move)

pgn_history = " ".join(pgn_moves) if pgn_moves else "Starting position"

# Get list of legal moves
legal_moves = [move.uci() for move in board.legal_moves]
legal_moves_str = ", ".join(legal_moves)

The system prompt restricts responses to UCI notation (4 characters, e.g., e2e4):

Google AdInline article slot
prompt = f"""You are playing chess as {'white' if board.turn == chess.WHITE else 'black'}.

Current position (FEN): {fen}
Move history: {pgn_history}
Move number: {self.move_count}

Available moves in UCI format: {legal_moves_str}
Choose the BEST move from available options and return ONLY the UCI move code (e.g., e2e4, g1f3, e7e8q).
Do not add explanations, analysis, or extra text.
The response must contain only the UCI move code."""

Combining FEN (current state), PGN (history), and UCI list (constraints) boosts accuracy by 80–90% compared to basic prompts.

Move Validation and Fallback Mechanism

LLMs may ignore instructions, returning text or illegal moves. The solution? Use the "AI Referee" pattern: python-chess validates UCI moves before applying them to the board.

UI loop logic:

Google AdInline article slot
if move_uci:
    try:
        move = chess.Move.from_uci(move_uci)
        if move in self.board.legal_moves:
            # Animation before executing move
            self.animate_move(move)
            self.board.push(move)
            self.move_history.append(move.uci())
            self.last_ai_response = f"LLM made move: {move.uci()}"
            print(self.last_ai_response)
        else:
            # Fallback to random move
            move = random.choice(list(self.board.legal_moves))
            self.animate_move(move)
            self.board.push(move)
            self.move_history.append(move.uci())
            self.last_ai_response = f"LLM failed, random move: {move.uci()}"
            print(self.last_ai_response)
    except ValueError:
        # Fallback to random move
        move = random.choice(list(self.board.legal_moves))

Example log:

  • Request #1: LLM responded with 'e7e5' → valid move.
  • Request #2: LLM replied with 'Good, I think the best move is d7d5' → fallback to a7a6.

This ensures uninterrupted gameplay without crashes.

Key Architecture Components

The project relies on:

  • python-chess: board management, move validation, FEN/PGN/UCI handling.
  • pygame: board rendering, move animations.
  • openai-client + OpenRouter: LLM inference calls.
  • Fallback logic: random.choice for resilience.

Modular design: the LLMAI class isolates LLM interaction from UI, passing only validated moves. Easily scalable to other rule-based games like Go or checkers.

| Component | Role | Library |

|-----------|------|---------|

| Board | State & validation | python-chess |

| LLM | Move selection | OpenRouter Llama 4 |

| UI | Rendering & input | pygame |

| Referee | Validation | try/except |

What Matters Most

  • OpenRouter offers free access to powerful LLMs for prototyping, with request volume caps.
  • A strict prompt using FEN, PGN, and UCI lists reduces hallucinations to 10–20%.
  • The "AI Referee" pattern with fallback ensures stability: LLM suggests, library verifies and applies.
  • python-chess is the de facto standard for chess logic in Python, covering all rules including castling and en passant.
  • This project demonstrates how to integrate non-deterministic models into deterministic applications.

— Editorial Team

Advertisement 728x90

Read Next