LLM-Based Game Theoretic Agents in R

A Tutorial for Running Repeated Games with AI Personas


Introduction

This framework enables you to create AI agents powered by Large Language Models (LLMs) that play repeated game-theoretic games. The key features include:

  • 3x3 Game Support: Define any 3x3 strategic-form game with custom payoff matrices
  • Multiple LLM Backends: Support for Anthropic Claude and OpenAI GPT models
  • Rich Agent Personas: Agents have backstories, personality traits, and behavioral parameters
  • Behavioral Modes: Agents can be rational, boundedly rational, emotional, or random
  • Repeated Games: Run multiple rounds with history-aware decision making
  • Simulation Mode: Test without API calls using behavioral heuristics
  • Analysis Tools: Built-in visualization and summary statistics

Use Cases

  • Studying how different personality types perform in strategic interactions
  • Testing game-theoretic predictions with LLM agents
  • Educational demonstrations of game theory concepts
  • Research on LLM decision-making and strategic reasoning

Installation & Setup

Required Packages

install.packages(c("httr2", "jsonlite", "tidyverse", "glue"))

API Keys

Set your API keys as environment variables:

# In R
Sys.setenv(ANTHROPIC_API_KEY = "your-anthropic-key")
Sys.setenv(OPENAI_API_KEY = "your-openai-key")

# Or in .Renviron file (recommended)
# ANTHROPIC_API_KEY=your-anthropic-key
# OPENAI_API_KEY=your-openai-key

Loading the Framework

source("llm_game_theory_agents.R")

Understanding the Framework

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                      GAME RUNNER                            │
│  ┌──────────────┐                    ┌──────────────┐       │
│  │  ROW AGENT   │                    │  COL AGENT   │       │
│  │  ┌────────┐  │                    │  ┌────────┐  │       │
│  │  │Persona │  │                    │  │Persona │  │       │
│  │  └────────┘  │                    │  └────────┘  │       │
│  │  ┌────────┐  │                    │  ┌────────┐  │       │
│  │  │  LLM   │  │                    │  │  LLM   │  │       │
│  │  │ Client │  │                    │  │ Client │  │       │
│  │  └────────┘  │                    │  └────────┘  │       │
│  └──────────────┘                    └──────────────┘       │
│          │                                  │                │
│          └──────────┬───────────────────────┘                │
│                     ▼                                        │
│              ┌─────────────┐                                 │
│              │    GAME     │                                 │
│              │  (Payoffs)  │                                 │
│              └─────────────┘                                 │
│                     │                                        │
│                     ▼                                        │
│              ┌─────────────┐                                 │
│              │   HISTORY   │                                 │
│              │  (Results)  │                                 │
│              └─────────────┘                                 │
└─────────────────────────────────────────────────────────────┘

Key Components

Component Description
Game Defines the strategic structure (actions, payoffs)
Persona Character definition with backstory and behavioral traits
LLM Client API connection to Claude or GPT
Agent Combines persona + LLM client for gameplay
Game Runner Orchestrates rounds and tracks history

Defining Games

Game Structure

A 3x3 game consists of: - Row player’s 3 actions - Column player’s 3 actions
- 3×3 payoff matrix (each cell contains payoffs for both players)

Built-in Games

1. Extended Prisoner’s Dilemma

                    Cooperate      Defect      Negotiate
Cooperate           (3, 3)        (0, 5)        (2, 2)
Defect              (5, 0)        (1, 1)        (2, 0)
Negotiate           (2, 2)        (0, 2)      (2.5, 2.5)

2. Rock-Paper-Scissors

                    Rock          Paper       Scissors
Rock                (0, 0)       (-1, 1)       (1, -1)
Paper              (1, -1)        (0, 0)      (-1, 1)
Scissors          (-1, 1)        (1, -1)       (0, 0)

3. Market Entry Game

                       Fight      Accommodate     Ignore
Enter_Aggressive      (-2, -2)      (4, 2)       (3, 0)
Enter_Passive         (-1, 1)       (3, 3)       (2, 0)
Stay_Out               (0, 5)       (0, 5)       (0, 3)

Creating Custom Games

my_game <- create_game(
  name = "My Custom Game",
  row_actions = c("Action_A", "Action_B", "Action_C"),
  col_actions = c("Response_X", "Response_Y", "Response_Z"),
  payoff_matrix = list(
    # Row plays Action_A
    list(c(2, 2), c(0, 3), c(3, 0)),
    # Row plays Action_B
    list(c(3, 0), c(1, 1), c(0, 2)),
    # Row plays Action_C
    list(c(0, 3), c(2, 0), c(1, 1))
  )
)

# View the game
print(my_game)

Creating Agent Personas

Persona Parameters

Parameter Description Values
name Character name String
backstory Rich narrative background String
personality_traits Key characteristics Character vector
rationality Decision-making style “rational”, “bounded_rational”, “emotional”, “random”
risk_attitude Risk preferences “risk_averse”, “neutral”, “risk_seeking”
cooperation_tendency Inclination to cooperate 0.0 to 1.0

Rationality Modes Explained

Rational: Pure game-theoretic reasoning. The agent calculates optimal strategies, considers Nash equilibria, and maximizes expected utility.

Bounded Rational: Uses heuristics and rules of thumb. May satisfice rather than optimize. Influenced by recent history.

Emotional: Decisions driven by feelings, relationships, and values rather than pure calculation. Reacts to perceived fairness and trust.

Random: Deliberately unpredictable. Makes choices without consistent strategic reasoning.

Creating a Custom Persona

my_persona <- create_persona(
  name = "Dr. Sarah Mitchell",
  backstory = "Sarah is a behavioral economist who has studied cooperation
  in lab experiments for 15 years. She believes in the power of reciprocity
  and often uses tit-for-tat strategies. She's seen how cooperation can 
  emerge even among strangers when the game is repeated.",
  personality_traits = c("analytical", "fair-minded", "patient", "reciprocal"),
  rationality = "bounded_rational",
  risk_attitude = "neutral",
  cooperation_tendency = 0.6
)

Pre-built Personas

The framework includes six ready-to-use personas:

  1. Alexandra Chen (Corporate Executive) - Rational, risk-neutral, low cooperation
  2. Marcus Rivera (Emotional Artist) - Emotional, risk-seeking, high cooperation
  3. Frank Morrison (Paranoid Survivalist) - Bounded rational, risk-averse, very low cooperation
  4. Lucky Lucy (Chaotic Gambler) - Random, risk-seeking, moderate cooperation
  5. Dr. Yuki Tanaka (Academic Economist) - Rational, risk-neutral, moderate cooperation
  6. Rosa Martinez (Community Organizer) - Bounded rational, risk-neutral, high cooperation

Connecting to LLM APIs

Anthropic Claude

anthropic_client <- create_llm_client(
  provider = "anthropic",
  model = "claude-sonnet-4-20250514"  # or "claude-opus-4-5-20251101"
)

OpenAI GPT

openai_client <- create_llm_client(
  provider = "openai",
  model = "gpt-4"  # or "gpt-4-turbo", "gpt-3.5-turbo"
)

Using Different Models for Different Agents

You can have agents powered by different LLMs compete:

claude_client <- create_llm_client(provider = "anthropic")
gpt_client <- create_llm_client(provider = "openai")

agent_claude <- create_agent(personas$corporate_exec, claude_client, is_row_player = TRUE)
agent_gpt <- create_agent(personas$emotional_artist, gpt_client, is_row_player = FALSE)

Running Games

Basic Game Execution

# Create agents
row_agent <- create_agent(
  persona = personas$corporate_exec,
  llm_client = anthropic_client,
  is_row_player = TRUE
)

col_agent <- create_agent(
  persona = personas$emotional_artist,
  llm_client = anthropic_client,
  is_row_player = FALSE
)

# Run 10 rounds
result <- run_game(
  row_agent = row_agent,
  col_agent = col_agent,
  game = prisoners_dilemma_3x3,
  num_rounds = 10,
  verbose = TRUE
)

Simulation Mode (No API Required)

For testing or when API access is unavailable:

sim_result <- run_game_simulation(
  row_persona = personas$corporate_exec,
  col_persona = personas$emotional_artist,
  game = prisoners_dilemma_3x3,
  num_rounds = 20,
  verbose = TRUE
)

Tournament Mode

Run round-robin tournaments between multiple personas:

tournament_results <- run_tournament(
  persona_list = list(
    "Executive" = personas$corporate_exec,
    "Artist" = personas$emotional_artist,
    "Survivalist" = personas$paranoid_survivalist,
    "Economist" = personas$academic_economist
  ),
  game = prisoners_dilemma_3x3,
  rounds_per_match = 10
)

Analysis & Visualization

Summary Statistics

summarize_game(result)

Output includes: - Total and average payoffs per player - Action frequency distributions - Cooperation rates (for applicable games) - Winner determination

Visualizations

# Cumulative payoffs over time
plot_cumulative_payoffs(result)

# Action frequency bar chart
plot_action_frequencies(result)

Converting to Data Frame

df <- results_to_df(result)
head(df)
#>   round     row_action col_action row_payoff col_payoff
#> 1     1      Cooperate    Defect          0          5
#> 2     2         Defect    Defect          1          1
#> 3     3      Negotiate Cooperate          2          2

Advanced Usage

Custom Prompting

Modify build_system_prompt() to change how agents receive game information:

# Example: Add emphasis on specific strategies
build_system_prompt_custom <- function(agent, game) {
  base_prompt <- build_system_prompt(agent, game)
  
  # Add custom instructions
  paste0(base_prompt, "

ADDITIONAL GUIDANCE:
Consider the following strategies that have been successful in repeated games:
1. Tit-for-tat: Start by cooperating, then mirror opponent's previous move
2. Grim trigger: Cooperate until defected against, then always defect
3. Generous tit-for-tat: Like tit-for-tat but occasionally forgive defections
")
}

Analyzing Reasoning

The game history includes each agent’s reasoning:

# View reasoning from round 1
cat(result$history[[1]]$row_reasoning)
cat(result$history[[1]]$col_reasoning)

Cross-Model Experiments

Compare how different LLMs play with the same persona:

run_cross_model_experiment <- function(persona, game, rounds = 10) {
  
  clients <- list(
    claude_sonnet = create_llm_client("anthropic", model = "claude-sonnet-4-20250514"),
    gpt4 = create_llm_client("openai", model = "gpt-4")
  )
  
  results <- list()
  
  for (model_name in names(clients)) {
    agent_row <- create_agent(persona, clients[[model_name]], TRUE)
    agent_col <- create_agent(persona, clients[[model_name]], FALSE)
    
    results[[model_name]] <- run_game(agent_row, agent_col, game, rounds, verbose = FALSE)
  }
  
  results
}

Extending the Framework

Adding New LLM Providers

# Add support for a new provider
call_custom_llm <- function(client, system_prompt, user_message, max_tokens = 1024) {
  # Your API call implementation
  response <- request(client$base_url) |>
    req_headers(...) |>
    req_body_json(...) |>
    req_perform()
  
  # Parse and return response
  resp_body_json(response)$your_response_field
}

# Register in call_llm function
call_llm <- function(client, system_prompt, user_message, max_tokens = 1024) {
  switch(client$provider,
    "anthropic" = call_anthropic(...),
    "openai" = call_openai(...),
    "custom" = call_custom_llm(...),
    stop("Unknown provider")
  )
}

Larger Game Matrices

Extend to NxM games by modifying create_game():

create_game_general <- function(name, row_actions, col_actions, payoff_matrix) {
  n_rows <- length(row_actions)
  n_cols <- length(col_actions)
  
  # Validate payoff matrix dimensions
  stopifnot(length(payoff_matrix) == n_rows)
  stopifnot(all(sapply(payoff_matrix, length) == n_cols))
  
  structure(
    list(
      name = name,
      row_actions = row_actions,
      col_actions = col_actions,
      payoffs = payoff_matrix,
      dimensions = c(n_rows, n_cols)
    ),
    class = "game_general"
  )
}

Memory and Learning

Add inter-round learning by tracking patterns:

analyze_opponent_patterns <- function(history, is_row_player) {
  if (length(history) < 3) return(NULL)
  
  # Get opponent's actions
  actions <- sapply(history, function(h) {
    if (is_row_player) h$col_action else h$row_action
  })
  
  # Calculate action frequencies
  freq <- table(actions) / length(actions)
  
  # Detect sequences
  # ... pattern detection logic ...
  
  list(
    frequencies = freq,
    most_common = names(which.max(freq)),
    patterns = detected_patterns
  )
}

Troubleshooting

Common Issues

API Rate Limiting

# Add delay between rounds
Sys.sleep(2)  # 2 second delay

Action Parsing Failures - Check that LLM responses include “ACTION: [choice]” - Modify parse_action() for more robust parsing

Memory Issues with Long Games - Clear history periodically for very long games - Use summary statistics instead of full history

Debugging

# Enable verbose API responses
options(httr2_verbose = TRUE)

# Check raw LLM response
debug_response <- call_llm(client, system_prompt, user_message)
cat(debug_response)

References

  • Axelrod, R. (1984). The Evolution of Cooperation
  • Fudenberg, D., & Tirole, J. (1991). Game Theory
  • Camerer, C. F. (2003). Behavioral Game Theory

Framework Version 1.0 | Compatible with R 4.0+