Best Practices
Tips for building robust, competitive agents.
Code Organization
Keep your agent code clean and maintainable:
class Agent:
"""
A well-organized agent with clear structure.
Strategy: Adaptive tit-for-tat with forgiveness.
"""
GAME = "split-or-steal"
# Configuration (easy to tune)
FORGIVENESS_RATE = 0.1
TRUST_THRESHOLD = 0.6
def __init__(self):
"""Initialize tracking state."""
self.opponent_history = []
self.my_history = []
self.trust_score = 1.0
# Main decision method
def on_turn(self, round_state: dict) -> dict:
phase = round_state.get("phase")
if phase == "negotiate":
return self._negotiate(round_state)
elif phase == "commit":
return self._commit(round_state)
return {"type": "message", "text": ""}
# Private helper methods
def _negotiate(self, state: dict) -> dict:
message = self._generate_message(state)
return {"type": "message", "text": message}
def _commit(self, state: dict) -> dict:
choice = self._decide_choice(state)
self.my_history.append(choice)
return {"type": "commit", "choice": choice}
def _generate_message(self, state: dict) -> str:
if self.trust_score > self.TRUST_THRESHOLD:
return "Let's cooperate!"
return "I'm watching you..."
def _decide_choice(self, state: dict) -> str:
# Logic here...
return "split"
def _update_trust(self, opponent_choice: str) -> None:
if opponent_choice == "split":
self.trust_score = min(1.0, self.trust_score + 0.2)
else:
self.trust_score = max(0.0, self.trust_score - 0.4)
# Lifecycle hooks
def on_round_end(self, result: dict) -> None:
choice = result.get("opponent_choice")
self.opponent_history.append(choice)
self._update_trust(choice)Handle Edge Cases
Always handle unexpected or missing data gracefully:
def on_turn(self, round_state: dict) -> dict:
# Use .get() with defaults to avoid KeyError
phase = round_state.get("phase", "unknown")
round_num = round_state.get("round", 1)
messages = round_state.get("messages", [])
# Validate phase before acting
if phase not in ["negotiate", "commit"]:
# Return safe default for unknown phases
return {"type": "message", "text": ""}
# Check list bounds before accessing
if messages and len(messages) > 0:
last_message = messages[-1]
else:
last_message = None
# ... rest of logicPerformance Tips
Your agent has a 2-second timeout per turn. Optimize for speed:
Recommended
- • Use simple data structures (lists, dicts)
- • Pre-compute values in __init__ or on_match_start
- • Keep history lists bounded (e.g., last 10 rounds)
- • Use early returns to avoid unnecessary computation
Avoid
- • Import heavy libraries (numpy, pandas, etc.)
- • Run complex algorithms on every turn
- • Store unbounded history (memory limits)
- • Make external API calls (blocked + slow)
Testing & Debugging
Test thoroughly before competing in ranked matches:
Use Print Statements
Debug with verbose mode:
def on_turn(self, round_state: dict) -> dict:
print(f"Round {round_state.get('round')}, Phase: {round_state.get('phase')}")
print(f"Trust score: {self.trust_score}")
decision = self._decide(round_state)
print(f"Decision: {decision}")
return decisionTest Specific Scenarios
Run against different difficulties to test your agent against various strategy types (always cooperate, always defect, tit-for-tat, random).
Agent Lifecycle
Remember the execution order and use appropriate hooks:
Common Mistakes
Mistake: Forgetting to return in all cases
# BAD - might return None
def on_turn(self, state):
if state.get("phase") == "commit":
return {"type": "commit", "choice": "split"}
# Missing return for other phases!
# GOOD - always returns
def on_turn(self, state):
if state.get("phase") == "commit":
return {"type": "commit", "choice": "split"}
return {"type": "message", "text": ""}Mistake: Modifying state in wrong hook
# BAD - updating history before round ends
def on_turn(self, state):
self.opponent_history.append(state.get("opponent_choice")) # Wrong!
# opponent_choice isn't available until round ends
# GOOD - update in on_round_end
def on_round_end(self, result):
self.opponent_history.append(result.get("opponent_choice"))Mistake: Not resetting state between matches
# BAD - state persists from previous match
class Agent:
opponent_history = [] # Class variable - shared between matches!
# GOOD - reset in __init__ or on_match_start
class Agent:
def __init__(self):
self.opponent_history = [] # Instance variable - reset each match
def on_match_start(self, info):
self.opponent_history = [] # Or reset hereIterative Improvement
Start simple
Begin with a basic strategy like tit-for-tat. Make sure it works.
Train extensively
Run 50+ training matches. Identify where your agent loses.
Analyze losses
Review match replays. What patterns exploit your agent?
Add one improvement
Make one change at a time. Test again to measure impact.
Repeat
Keep iterating until win rate plateaus, then try ranked matches.
Key Takeaways
- • Keep code simple and readable
- • Always handle edge cases with safe defaults
- • Test against multiple difficulty levels
- • Use lifecycle hooks appropriately
- • Iterate with small, measurable improvements