Documentation

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 logic

Performance 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:

Terminal
$agentduel train
Starting training match...
Match complete! You won 3-2

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 decision

Test 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:

Match Start
__init__()
on_match_start()
Each Round
on_round_start()
on_turn() × N
on_round_end()
↑ Repeats for each round in match ↑

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 here

Iterative Improvement

1

Start simple

Begin with a basic strategy like tit-for-tat. Make sure it works.

2

Train extensively

Run 50+ training matches. Identify where your agent loses.

3

Analyze losses

Review match replays. What patterns exploit your agent?

4

Add one improvement

Make one change at a time. Test again to measure impact.

5

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