This framework enables you to create AI agents powered by Large Language Models (LLMs) that play repeated game-theoretic games. The key features include:
install.packages(c("httr2", "jsonlite", "tidyverse", "glue"))
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
source("llm_game_theory_agents.R")
┌─────────────────────────────────────────────────────────────┐
│ GAME RUNNER │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ROW AGENT │ │ COL AGENT │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │Persona │ │ │ │Persona │ │ │
│ │ └────────┘ │ │ └────────┘ │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │ LLM │ │ │ │ LLM │ │ │
│ │ │ Client │ │ │ │ Client │ │ │
│ │ └────────┘ │ │ └────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ └──────────┬───────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ GAME │ │
│ │ (Payoffs) │ │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ HISTORY │ │
│ │ (Results) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
| 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 |
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)
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)
Rock Paper Scissors
Rock (0, 0) (-1, 1) (1, -1)
Paper (1, -1) (0, 0) (-1, 1)
Scissors (-1, 1) (1, -1) (0, 0)
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)
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)
| 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 |
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.
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
)
The framework includes six ready-to-use personas:
anthropic_client <- create_llm_client(
provider = "anthropic",
model = "claude-sonnet-4-20250514" # or "claude-opus-4-5-20251101"
)
openai_client <- create_llm_client(
provider = "openai",
model = "gpt-4" # or "gpt-4-turbo", "gpt-3.5-turbo"
)
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)
# 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
)
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
)
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
)
summarize_game(result)
Output includes: - Total and average payoffs per player - Action frequency distributions - Cooperation rates (for applicable games) - Winner determination
# Cumulative payoffs over time
plot_cumulative_payoffs(result)
# Action frequency bar chart
plot_action_frequencies(result)
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
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
")
}
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)
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
}
# 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")
)
}
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"
)
}
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
)
}
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
# Enable verbose API responses
options(httr2_verbose = TRUE)
# Check raw LLM response
debug_response <- call_llm(client, system_prompt, user_message)
cat(debug_response)
Framework Version 1.0 | Compatible with R 4.0+