7.4 - Saving Data Between Matches
For a bot to learn, adapt, and improve over time, it must have a memory. It needs a way to store information from one match and retrieve it in the next. This capability, known as persistence, is the foundation of any learning AI.
While python-sc2
does not provide a built-in database, it fully supports standard Python file operations, allowing you to easily read and write data to your local disk.
The Persistence Lifecycle: Read, Update, Write
The process of managing persistent data follows a simple, three-stage lifecycle that mirrors your bot's own.
+--------------------------+
| 1. Read on Start | <-- In on_start(), load data from a file
| (Load Knowledge) | into a `self` variable.
+--------------------------+
|
v
+--------------------------+
| 2. Update in `on_step` | <-- As the game progresses, modify the
| (Gain Experience) | data in the `self` variable.
+--------------------------+
|
v
+--------------------------+
| 3. Write on End | <-- In on_end(), save the updated `self`
| (Record Knowledge) | variable back to the file.
+--------------------------+
By following this pattern, your bot creates a feedback loop, where the outcome of each game informs its strategy for the next.
Choosing Your Data Format: json
vs. pickle
You have two primary choices for how to format your data on disk. Your choice depends on the complexity of the data you need to store.
Aspect | json (JavaScript Object Notation) | pickle (Python Specific) |
---|---|---|
Data Format | Human-readable text. | Binary, not human-readable. |
Use Case | Recommended for most bots. Ideal for simple, structured data like dictionaries, lists, and numbers. | For storing complex, custom Python objects or class instances that json cannot handle. |
Pros | - Easy to open and debug by hand. - Language-agnostic. | - Can serialize almost any Python object. |
Cons | - Limited to simple data types. | - Can be a security risk if loading files from an untrusted source. - Tied to a specific Python version. |
The Professional Recommendation: Start with json
. Its readability makes debugging far easier. Only move to pickle
if you have a specific, complex object that json
cannot represent.
A Developer's Checklist for Persistence
- 1. Define Your Data Structure: What information do I need to save? (e.g., a dictionary of strategies with win/loss counts).
- 2. Encapsulate File Logic: Create a helper class or functions to handle reading and writing. This keeps your main bot class clean.
- 3. Implement the
on_start
Hook: Load the data from the file into aself
variable. Handle the case where the file doesn't exist yet. - 4. Implement the
on_end
Hook: Save the updatedself
variable back to the file.
Code Example: The "Adaptive" Bot with an Encapsulated Knowledge Base
This bot demonstrates a professional approach to persistence. It uses a dedicated KnowledgeBase
class to manage all file I/O for its data.json
file. The bot uses this knowledge to choose the strategy with the best historical win rate, and updates the file with the result of each match.
To see this bot learn, you must run the script multiple times.
# adaptive_bot.py
import json
from pathlib import Path
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.data import Difficulty, Race, Result
from sc2.main import run_game
from sc2.player import Bot, Computer
from typing import Dict
# A dedicated class to handle all data persistence logic.
class KnowledgeBase:
def __init__(self, file_path: Path):
self.file_path = file_path
# Define the default structure of our knowledge base.
self.data: Dict[str, Dict[str, int]] = {
"aggressive": {"wins": 0, "losses": 0},
"defensive": {"wins": 0, "losses": 0},
}
def load(self):
"""Loads data from the file, handling potential errors."""
try:
# Check if the file exists and is not empty.
if self.file_path.is_file() and self.file_path.stat().st_size > 0:
with open(self.file_path, "r") as f:
self.data = json.load(f)
print(f"Knowledge loaded from {self.file_path}")
else:
print("No knowledge file found or file is empty. Starting fresh.")
except json.JSONDecodeError:
# Handle cases where the file is corrupted or not valid JSON.
print(f"Error decoding JSON from {self.file_path}. Starting with fresh knowledge.")
except Exception as e:
# Handle other potential file reading errors.
print(f"An unexpected error occurred while loading knowledge: {e}. Starting fresh.")
def save(self):
"""Saves the current data to the file."""
try:
with open(self.file_path, "w") as f:
json.dump(self.data, f, indent=4)
print(f"Knowledge saved to {self.file_path}")
except Exception as e:
print(f"An unexpected error occurred while saving knowledge: {e}.")
def update_result(self, strategy: str, result: Result):
"""Updates the win/loss count for a given strategy."""
if result == Result.Victory:
self.data[strategy]["wins"] += 1
else:
self.data[strategy]["losses"] += 1
def choose_strategy(self) -> str:
"""Chooses the strategy with the best historical win rate."""
agg_wins = self.data["aggressive"]["wins"]
agg_total = sum(self.data["aggressive"].values()) or 1
def_wins = self.data["defensive"]["wins"]
def_total = sum(self.data["defensive"].values()) or 1
return "aggressive" if (agg_wins / agg_total) >= (def_wins / def_total) else "defensive"
# The main bot class now uses the KnowledgeBase.
class AdaptiveBot(BotAI):
def __init__(self):
super().__init__()
# Define a path for the data file relative to this script's location.
# This makes the bot's data portable and independent of the launch directory.
knowledge_path = Path(__file__).parent / "bot_data.json"
self.knowledge = KnowledgeBase(knowledge_path)
self.current_strategy = None
async def on_start(self):
"""Load knowledge and choose a strategy for this match."""
self.knowledge.load()
self.current_strategy = self.knowledge.choose_strategy()
print(f"Chosen strategy for this match: {self.current_strategy.upper()}")
async def on_step(self, iteration: int):
"""Execute the chosen strategy."""
# A very simple aggressive strategy for demonstration purposes.
if self.current_strategy == "aggressive" and self.units.idle.amount > 10:
for unit in self.units.idle:
unit.attack(self.enemy_start_locations[0])
async def on_end(self, game_result: Result):
"""Update and save knowledge with the result of this match."""
print(f"Match ended with result: {game_result}")
self.knowledge.update_result(self.current_strategy, game_result)
self.knowledge.save()
if __name__ == "__main__":
run_game(
maps.get("GresvanAIE"),
[
Bot(Race.Terran, AdaptiveBot()),
Computer(Race.Zerg, Difficulty.Easy)
],
realtime=False,
)