A high-performance Rust implementation of an iterated Prisoner's Dilemma tournament, where different strategies compete against each other in a round-robin fashion. Inspired by The Selfish Gene by Richard Dawkins and Axelrod's seminal work on the evolution of cooperation.
The Prisoner's Dilemma is a fundamental concept in game theory. This simulator runs a tournament where different strategies play against each other thousands of times, allowing you to analyze which strategies perform best under various conditions.
In each round, two players simultaneously choose to either Cooperate or Defect:
| Outcome | Payoff |
|---|---|
| Both Cooperate | 2 points each |
| Both Defect | 1 point each |
| One Cooperates, One Defects | Defector gets 3, Cooperator gets 0 |
The key insight: mutual cooperation yields the best collective outcome (4 total points), but defection is always individually rational.
- Multi-threaded Execution: Automatically uses all available CPU cores for fast tournament simulation
- Configurable Parameters: Adjust thread count, iteration count, and strategy selection via CLI
- Multiple Strategies: Test built-in strategies or easily add your own
- Verbose Reporting: Optional detailed output for tournament analysis
- Fast Tournament Play: Plays millions of iterations efficiently
- Rust 1.70 or later (install Rust)
cargo build --releaseThe compiled binary will be at target/release/prisoners_dilemma.
Run a tournament with all default strategies:
cargo run --releasecargo run --release -- [OPTIONS]-t, --threads <THREADS>: Number of threads to use (default: number of CPU cores)-i, --iterations <ITERATIONS>: Number of iterations per game (default: 1,000,000)-s, --strategies <STRATEGIES>: Comma-separated list of strategies to include-v, --verbose: Show additional tournament details
Run with specific strategies:
cargo run --release -- --strategies tit-for-tat,always-defect,randomRun with fewer iterations (faster, for testing):
cargo run --release -- --iterations 10000Run with custom thread count and verbose output:
cargo run --release -- --threads 4 --iterations 100000 --verboseLimit to two strategies:
cargo run --release -- --strategies always-cooperate,always-defect --iterations 1000000 --verboseAlways plays Cooperate, regardless of the opponent's moves. Vulnerable to exploitation but promotes cooperation.
Always plays Defect, regardless of the opponent's moves. Guarantees individual payoff but prevents mutual cooperation.
Cooperates on the first move, then copies the opponent's previous move on subsequent rounds. A simple, highly effective strategy that is nice (never defects first), retaliatory (punishes defection), and forgiving (returns to cooperation).
Randomly chooses to Cooperate or Defect with 50/50 probability. Useful for testing robustness of strategies.
Similar to Tit for Tat but requires two consecutive defections from the opponent before retaliating. More forgiving than Tit for Tat, allowing recovery from mutual misunderstandings.
Each line of output represents one matchup result:
TitForTat vs Always Defect: 999000 vs 1000001
This means:
- TitForTat scored 999,000 points
- Always Defect scored 1,000,001 points
- The matchup ran for the specified number of iterations
Note: In a tournament, each strategy plays against every other strategy (including itself), so the total number of lines equals num_strategies².
- Strategy Trait: Defines the interface for implementing game strategies
- Player: Manages a strategy and tracks score throughout a game
- PrisonerDilemmaGame: Executes the game, manages history, and calculates payoffs
- ThreadPool: Distributes games across available CPU cores
- Arc (Atomic Reference Counting): Safely shares strategy objects across threads
- MPSC Channels: Collects results from worker threads
- Trait Objects: Allows polymorphic strategy behavior without compile-time specialization
The simulator is optimized for speed:
- Multi-threaded: Automatically parallelizes across all CPU cores
- Efficient History Tracking: Only keeps necessary move history
- Static Dispatch: Uses trait objects with minimal overhead
On modern hardware, expect to simulate ~1 million iterations per strategy pair in a few seconds.
To add a new strategy, implement the Strategy trait:
pub struct MyStrategy;
impl Strategy for MyStrategy {
fn play(&self, hist: &History, hist_inx: usize) -> Move {
// hist contains all previous moves: hist[i] = [player1_move, player2_move]
// hist_inx is 0 for player 1, 1 for player 2
// Return your strategy's move
Move::Cooperate
}
fn name(&self) -> String {
"My Strategy".to_string()
}
}Then add it to the strategies vector in the parse_strategies() function or directly in main.
The project is easily testable with reduced iteration counts:
cargo run --release -- --iterations 1000 --verboseprisoners_dilemma/
├── Cargo.toml # Project manifest and dependencies
├── Cargo.lock # Locked dependency versions
├── README.md # This file
└── src/
└── main.rs # All implementation (single-file project)
- clap: Command-line argument parsing with derive macros
- num_cpus: Detects available CPU cores
- rand: Random number generation for Random strategy
- threadpool: Thread pool implementation for parallel execution
This project is provided as-is for educational purposes.
- Prisoner's Dilemma on Wikipedia
- Axelrod's Tournament - The classic tournament study
Potential improvements for the project:
- Export results to CSV or JSON format
- Calculate aggregate statistics (total score, win rate, etc.)
- Support for weighted tournament (multiple rounds with different iterations)
- Custom payoff matrix configuration
- Strategy performance analysis and ranking
- Interactive mode for designing custom strategies