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.
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):
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:
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.choicefor 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
No comments yet.