Client + MCTS helpers for board game AI integration with self-play support.
Project description
Game AI Client SDK
A Python SDK for integrating AI into turn-based board games using Monte Carlo Tree Search (MCTS). This framework provides a generic interface that allows game developers to add AI opponents to their games with minimal coupling to game-specific logic.
Features
- Unified Game Interface: Simplified architecture - TurnBasedGame works directly with MCTS
- MCTS AI Engine: Powerful AI using Monte Carlo Tree Search with customizable strategies
- Optional Tree Reuse: 40-60% performance boost when games implement
__eq__(completely optional!) - AI vs AI Gameplay: Built-in support for AI vs AI matches with configurable difficulty levels
- Three Difficulty Levels: Easy, Medium, and Hard AI opponents with different playing strengths
- Design Patterns: Built with Strategy and State patterns for extensibility
- Event Logging: Optional match and move logging via RabbitMQ
- Minimal Integration: Keep full control of your game logic
Installation
pip install game-ai-client==0.1.2
Quick Start
Basic Usage
Import the SDK:
from game_sdk import AIGameClient
from game_sdk.utils import build_generic_state
AI vs AI Quick Start
For games implementing TurnBasedGame, you can immediately run AI vs AI matches:
from your_game import YourGame, state_to_game, game_to_state
# Create initial game
game = YourGame(initial_board, players, starting_player)
# Run AI vs AI match
result = game.ai_vs_ai_difficulty_selection(
difficulty1="medium",
difficulty2="hard",
state_to_game_fn=state_to_game,
game_to_state_fn=game_to_state,
game_id="your_game"
)
print(f"Winner: {result['winner']}")
Available difficulties: "easy", "medium", "hard"
RabbitMQ Setup (Optional)
The SDK includes built-in event logging to RabbitMQ. To enable it, start RabbitMQ using Docker:
docker-compose up -d
This will start RabbitMQ on:
- AMQP port: 6000
- Management UI: http://localhost:15672
Configure via environment variables:
export RABBITMQ_HOST=localhost
export RABBITMQ_PORT=6000
export RABBITMQ_USER=guest
export RABBITMQ_PASS=guest
If RabbitMQ is unavailable, the SDK gracefully falls back to stdout logging.
Integration Guide
You keep full control over your game logic. To use the framework, you only need to provide:
- Players list
- Legal moves function
- Apply-move function for AI state transitions
- Players list:
players = [
{"id": "P1", "type": "human", "symbol": "X"},
{"id": "P2", "type": "ai_mcts","symbol": "O"},
]
- Legal moves function Return all legal moves for the current player, as a list of dicts:
def compute_legal_moves(board, players, current_player_symbol):
player_index = next(i for i, p in enumerate(players)
if p["symbol"] == current_player_symbol)
moves = []
for r in range(len(board)):
for c in range(len(board[0])):
if board[r][c].strip() == "":
moves.append({
"id": f"PLACE_{r}_{c}", # string id is recommended
"player_index": player_index, # index in players[]
"type": "PLACE_MARK",
"position": {"row": r, "col": c},
})
return moves
Apply-Move Function
This is the only function needed from your game to enable AI. It takes a generic state and move, returns a new state:
from typing import Dict, Any, List
from game_sdk.utils import build_generic_state
State = Dict[str, Any]
Move = Dict[str, Any]
def apply_move_my_game(state: State, move: Move) -> State:
players: List[Dict[str, Any]] = state["players"]
# --- decode board from generic state ---
board_info = state["board"]
int_board = board_info["cells"] # 2D ints
symbol_map = {0: " "}
for idx, p in enumerate(players):
symbol_map[idx + 1] = p["symbol"]
board = [[symbol_map[v] for v in row] for row in int_board]
# --- apply the move ---
r = move["position"]["row"]
c = move["position"]["col"]
player_index = move["player_index"]
symbol = players[player_index]["symbol"]
board[r][c] = symbol
# --- next player ---
next_player_index = (player_index + 1) % len(players)
next_symbol = players[next_player_index]["symbol"]
# --- terminal & result: YOUR logic here ---
# is_terminal_and_winner() should be implemented by you and return:
# (finished: bool, winner_symbol: Optional[str])
finished, winner_symbol = is_terminal_and_winner(board)
result_map = None
if finished:
# winner_to_results() should return (result_str, result_map)
# e.g. result_map = {"P1": 1.0, "P2": -1.0}
_, result_map = winner_to_results(winner_symbol, players)
move_count = state["extra"].get("move_count", 0) + 1
legal_moves = [] if finished else compute_legal_moves(board, players, next_symbol)
# --- build and return the new state ---
return build_generic_state(
game_id=state["game_id"],
board=board,
players=players,
current_player_symbol=next_symbol,
move_count=move_count,
finished=finished,
legal_moves=legal_moves,
result=result_map,
)
Building a state for the AI / logging
On each turn, describe the current position using build_generic_state:
state = build_generic_state(
game_id="my_game_id",
board=board, # 2D list of symbols, e.g. [["X"," ","O"], ...]
players=players,
current_player_symbol=current_sym, # whose turn it is
move_count=move_count,
finished=False, # or True if you know it’s over
legal_moves=compute_legal_moves(board, players, current_sym),
result=None, # for terminal state: {player_id: score}
)
Using AIGameClient in your loop
Create client and start match:
client = AIGameClient(
game_id="my_game_id",
api_key="demo-key",
apply_move_fn=apply_move_my_game,
)
match_id = client.start_match(players=players, metadata={"mode": "casual"})
Each turn:
while not finished:
current_sym = ... # your turn logic
legal_moves = compute_legal_moves(board, players, current_sym)
state = build_generic_state(
game_id="my_game_id",
board=board,
players=players,
current_player_symbol=current_sym,
move_count=move_count,
finished=False,
legal_moves=legal_moves,
)
current_player = next(p for p in players if p["symbol"] == current_sym)
if current_player["type"] == "ai_mcts":
# ---- AI turn ----
client.send_state(match_id, state)
move = client.best_move(match_id, iterations=800)
else:
# ---- human turn ----
move = get_human_move_somehow(legal_moves)
# Apply move in your game
apply_move_on_real_board(board, move, current_sym)
# log the move
client.log_move(match_id=match_id, state=state, move=move)
# Update finished / winner using your own logic
finished, winner_symbol = is_terminal_and_winner(board)
move_count += 1
End the match:
result_str, result_map = winner_to_results(winner_symbol, players)
final_state = build_generic_state(
game_id="my_game_id",
board=board,
players=players,
current_player_symbol=current_sym,
move_count=move_count,
finished=True,
legal_moves=[],
result=result_map,
)
client.end_match(
match_id=match_id,
result=result_str,
final_state=final_state,
)
Architecture
Design Patterns
The SDK implements multiple design patterns for extensibility and maintainability:
Strategy Pattern (MCTS Customization)
The MCTS algorithm is decomposed into four pluggable strategies:
from game_sdk.mcts import (
MCTSStrategy,
DefaultSelectionStrategy,
DefaultExpansionStrategy,
DefaultSimulationStrategy,
DefaultBackpropagationStrategy,
)
# Use custom strategies
custom_mcts = MCTSStrategy(
selection_strategy=DefaultSelectionStrategy(),
expansion_strategy=DefaultExpansionStrategy(),
simulation_strategy=DefaultSimulationStrategy(),
backpropagation_strategy=DefaultBackpropagationStrategy(),
)
Each strategy can be independently customized:
- SelectionStrategy: How to traverse the tree (default: UCB1)
- ExpansionStrategy: How to add nodes (default: single unexpanded move)
- SimulationStrategy: How to simulate games (default: random rollout)
- BackpropagationStrategy: How to update values (default: visit counts + rewards)
State Pattern (MCTS Phases)
MCTS iterations follow a state machine:
- Selection Phase → Expansion Phase → Simulation Phase → Backpropagation Phase → Complete
Each phase handles its work and transitions to the next state, providing clean separation of concerns.
Protocol-Based Design
The SDK uses Python protocols for game integration. Implement the TurnBasedGame protocol:
from game_sdk.ai_client import TurnBasedGame, Move
from typing import List
class MyGame(TurnBasedGame):
def current_player(self) -> int:
# Return current player index (0, 1, etc.)
return 0 # Your logic here
def get_legal_actions(self) -> List[Move]:
# Return all legal moves
pass
def is_game_over(self) -> bool:
# Check if game is finished
pass
def game_result(self) -> float:
# Return result: 1.0 (win), -1.0 (loss), 0.0 (draw) from current player perspective
pass
def move(self, action: Move) -> "MyGame":
# Apply move and return new game state (immutable)
pass
# Optional: Implement for 40-60% performance boost with tree reuse!
def __eq__(self, other) -> bool:
# Compare game states for equality
return isinstance(other, MyGame) and self.state == other.state
def __hash__(self) -> int:
# Make game hashable (required if implementing __eq__)
return hash(self.state) # Use immutable representation
Component Overview
┌─────────────────────────────────────────────┐
│ Game Implementation │
│ - Your game logic │
│ - TurnBasedGame protocol │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ AIGameClient │
│ - State management │
│ - MCTS integration │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ MCTS Engine (Strategy Pattern) │
│ - Selection → Expansion │
│ - Simulation → Backpropagation │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Event Logging (RabbitMQ) │
│ - Match lifecycle events │
│ - Move logging │
└─────────────────────────────────────────────┘
Advanced Usage
Customizing MCTS Parameters
Control the AI strength and behavior:
# More iterations = stronger AI (but slower)
move = client.best_move(match_id, iterations=1000)
# Fewer iterations = faster but weaker AI
move = client.best_move(match_id, iterations=100)
AI vs AI Gameplay with Difficulty Levels
The SDK provides built-in support for AI vs AI matches with three difficulty levels:
- Easy: 100 MCTS iterations, 50% random moves (beginner-friendly)
- Medium: 1000 MCTS iterations, pure MCTS strategy (intermediate)
- Hard: 5000 MCTS iterations, near-perfect play (expert)
Using the TurnBasedGame Protocol
Any game implementing the TurnBasedGame protocol automatically gets AI vs AI functionality:
from game_sdk.ai_client import TurnBasedGame
from typing import List, Dict, Any
class MyGame(TurnBasedGame):
def current_player(self) -> int:
# Return current player index (0, 1, etc.)
pass
def get_legal_actions(self) -> List[Dict[str, Any]]:
# Return list of legal moves
pass
def is_game_over(self) -> bool:
# Check if game has ended
pass
def game_result(self) -> float:
# Return 1.0 (win), -1.0 (loss), or 0.0 (draw) from current player's perspective
pass
def move(self, action: Dict[str, Any]) -> "MyGame":
# Apply move and return new game state (immutable)
pass
Running AI vs AI Matches
Once your game implements TurnBasedGame, you can run AI vs AI matches:
# Create initial game state
game = MyGame(initial_board, players, current_player_symbol)
# Play AI vs AI with different difficulties
result = game.ai_vs_ai_difficulty_selection(
difficulty1="easy", # First AI plays at easy difficulty
difficulty2="hard", # Second AI plays at hard difficulty
state_to_game_fn=state_to_game_converter,
game_to_state_fn=game_to_state_converter,
game_id="my_game",
verbose=True # Print game progress
)
# Check results
print(f"Winner: {result['winner']}")
print(f"Total moves: {result['move_count']}")
print(f"Move history: {result['move_history']}")
Converter Functions
You need two adapter functions to convert between your game and the generic SDK state:
def state_to_game_converter(state: Dict[str, Any]) -> MyGame:
"""Convert SDK state dictionary to your game object."""
board = extract_board_from_state(state)
players = state["players"]
current_player = players[state["turn_index"]]["symbol"]
return MyGame(board, players, current_player)
def game_to_state_converter(game: MyGame, prev_state: Dict[str, Any]) -> Dict[str, Any]:
"""Convert your game object back to SDK state dictionary."""
from game_sdk.utils import build_generic_state
return build_generic_state(
game_id=prev_state["game_id"],
board=game.board,
players=game.players,
current_player_symbol=game.current_player_symbol,
move_count=prev_state.get("extra", {}).get("move_count", 0) + 1,
finished=game.is_game_over(),
legal_moves=[] if game.is_game_over() else game.get_legal_actions(),
result=compute_result_map(game) if game.is_game_over() else None
)
Example: Running a Tournament
from typing import Dict
def run_tournament(difficulty1: str, difficulty2: str, num_games: int = 10) -> Dict:
"""Run multiple AI vs AI games and collect statistics."""
wins = {"Player1": 0, "Player2": 0, "Draw": 0}
for i in range(num_games):
game = MyGame(create_empty_board(), players, "X")
result = game.ai_vs_ai_difficulty_selection(
difficulty1=difficulty1,
difficulty2=difficulty2,
state_to_game_fn=state_to_game_converter,
game_to_state_fn=game_to_state_converter,
game_id="my_game",
verbose=False # Silent mode
)
if result['winner']:
winner_idx = 0 if result['winner'] == "X" else 1
key = f"Player{winner_idx + 1}"
wins[key] += 1
else:
wins["Draw"] += 1
return wins
# Run tournament
stats = run_tournament("medium", "hard", num_games=20)
print(f"Results: {stats}")
Complete Example Files
See the example files for complete working implementations:
ai_vs_ai_example.py: Simple AI vs AI game examplesgame_manager.py: Full game management system with tournamentstictactoe.py: Complete Tic-Tac-Toe implementation with AI
Generic State Format
The SDK uses a standardized state representation:
{
"game_id": "tictactoe",
"state_id": "unique-uuid",
"turn_index": 5,
"players": [
{"id": "P1", "type": "human", "symbol": "X"},
{"id": "P2", "type": "ai_mcts", "symbol": "O"}
],
"board": {
"representation": "grid",
"rows": 3,
"cols": 3,
"cells": [[1, 0, 2], [0, 1, 0], [2, 0, 0]],
"legend": {
"0": "empty",
"1": "player_1_piece",
"2": "player_2_piece"
}
},
"status": "IN_PROGRESS",
"is_terminal": false,
"legal_moves": [...],
"result": null,
"extra": {"move_count": 5}
}
Example Implementation
See tictactoe.py for a complete working example demonstrating:
- TurnBasedGame protocol implementation
- Human vs AI gameplay
- AI vs AI gameplay with multiple difficulty levels
- State management
- Event logging
Tree Reuse (Performance Optimization)
Tree reuse is an optional optimization that can make your AI 40-60% faster on subsequent moves.
How It Works
Without tree reuse, MCTS rebuilds the entire search tree from scratch for every move. With tree reuse, MCTS remembers and reuses the tree from previous searches.
Enabling Tree Reuse
Simply implement __eq__ and __hash__ in your game class:
class MyGame(TurnBasedGame):
# ... required methods ...
def __eq__(self, other):
if not isinstance(other, MyGame):
return False
# Compare all game state fields
return (
self.board == other.board and
self.current_player_symbol == other.current_player_symbol
)
def __hash__(self):
# Convert board to immutable type for hashing
board_str = ''.join(''.join(row) for row in self.board)
return hash((board_str, self.current_player_symbol))
That's it! Tree reuse now works automatically. If you don't implement these methods, your game still works perfectly - it just won't get the speed boost.
Usage
from game_sdk.mcts import MCTS
# Tree reuse enabled by default
mcts = MCTS(exploration_c=1.4, enable_tree_reuse=True)
# Play multiple moves - tree is reused automatically!
game = MyGame(...)
move1 = mcts.search(game, iterations=1000) # Builds tree
game = game.move(move1)
move2 = mcts.search(game, iterations=1000) # Reuses tree! Much faster
# Clear cache between games
mcts.clear_cache()
When to Use
✅ Use tree reuse when:
- Playing sequential moves in the same game
- Performance is important
- Your game state can be compared efficiently
❌ Don't worry about it when:
- Quick prototyping
- Game state comparison is complex
- You want to keep code simple
Note: Tree reuse is completely optional - your game works either way!
For more details, see TREE_REUSE_GUIDE.md and tree_reuse_example.py.
Changelog
Version 0.2.0 (Latest)
Major Refactoring:
- Unified Architecture: Merged
GameEnvandTurnBasedGameinterfaces- MCTS now works directly with
TurnBasedGameobjects (no adapter needed!) - Simplified architecture - one interface instead of two
- Backward compatible with
StatefulGameAdapterfor dict-based states
- MCTS now works directly with
- Tree Reuse: Added optional tree reuse for 40-60% performance improvement
- Completely optional - works with or without
__eq__implementation - Automatic if game implements
__eq__and__hash__ - Graceful fallback if not implemented (no errors)
- New
enable_tree_reuseparameter (default: True) - New
clear_cache()method to clear cached trees
- Completely optional - works with or without
API Changes:
- Added
current_player()method toTurnBasedGameprotocol (required) - Changed
game_result()return type frominttofloat - Removed
envparameter fromMCTSStrategy.__init__()(deprecated) - Added
enable_tree_reuseparameter toMCTSandAIGameClient
New Documentation:
TREE_REUSE_GUIDE.md: Comprehensive tree reuse guidetree_reuse_example.py: Working examples with/without tree reuse- Updated README with new architecture and features
Migration Guide: Existing games need to add:
current_player() -> intmethod (required)- Update
game_result()to returnfloatinstead ofint(required) - Optionally add
__eq__and__hash__for tree reuse (optional performance boost)
Version 0.1.7
Bug Fixes:
- Fixed winner determination logic in
ai_vs_ai_difficulty_selection()method- Previously, winners were incorrectly attributed due to inverted logic in result interpretation
- Hard difficulty now correctly demonstrates unbeatable play
- Results are now properly assigned when
game_result()returns-1(current player lost)
Improvements:
- Made RabbitMQ dependency optional
- SDK now gracefully handles missing
pikamodule - Falls back to stdout logging when RabbitMQ is unavailable
- Allows usage without message bus infrastructure
- SDK now gracefully handles missing
New Examples:
ai_vs_ai_example.py: Demonstrates AI vs AI gameplaygame_manager.py: Tournament management systemtest_difficulty_fix.py: Validation tests for difficulty levels
Requirements
- Python 3.9+
- pika 1.3.2 (optional, for RabbitMQ logging)
Credits
Tic-Tac-Toe example adapted from: https://gist.github.com/qianguigui1104/edb3b11b33c78e5894aad7908c773353
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file game_ai_client-0.4.0.tar.gz.
File metadata
- Download URL: game_ai_client-0.4.0.tar.gz
- Upload date:
- Size: 42.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09ac05605bf9f8cf76c8297f7a28279b3a7fb533bc6ec87f949f00587e9a8548
|
|
| MD5 |
e65921775e9e07d961f3d6796632116f
|
|
| BLAKE2b-256 |
241bac86a68945299d09b0ea7c94a7325e998c8c605dcd72054cb0d6d9031ade
|
File details
Details for the file game_ai_client-0.4.0-py3-none-any.whl.
File metadata
- Download URL: game_ai_client-0.4.0-py3-none-any.whl
- Upload date:
- Size: 48.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3e384d461e972a0e83c79b08c29e668a2dd873d51fd247bf2ba4e0dd3dab8ea
|
|
| MD5 |
c79aa9120370eb61fad1e58375787f03
|
|
| BLAKE2b-256 |
b7caac77ba86aa342adf6444f2cc2582cdd6fa50fde6e6568ba26232472b0355
|