Metadata-Version: 2.4
Name: evolutionary-game-dynamics
Version: 0.1.0
Summary: A Python library for simulating two-player strategic interactions with memory-based decision-making, exogenous shocks, and recovery dynamics.
Author-email: Ankur Tutlani <ankur.tutlani@gmail.com>
License: MIT License
        
        Copyright (c) 2026 ankur-tutlani
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/ankur-tutlani/evolutionary-game-dynamics
Project-URL: Documentation, https://github.com/ankur-tutlani/evolutionary-game-dynamics#readme
Project-URL: Repository, https://github.com/ankur-tutlani/evolutionary-game-dynamics.git
Project-URL: Issues, https://github.com/ankur-tutlani/evolutionary-game-dynamics/issues
Keywords: game-theory,evolutionary-dynamics,agent-based-simulation,monte-carlo,agent-based-modeling
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=2.0
Requires-Dist: pandas>=2.0
Requires-Dist: matplotlib>=3.5
Requires-Dist: seaborn>=0.12
Requires-Dist: openpyxl>=3.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# Game Theory Simulation Library

## Overview

This Python library simulates two-player strategic interactions with memory-based decision-making, exogenous shocks, and recovery dynamics. It is designed for research on **norm formation, stability, and recovery** following temporary disruptions to the payoff environment.

The project enables researchers to conduct large-scale Monte Carlo simulations across parameter spaces, analyze how agents' behavioral patterns respond to shocks, and measure recovery times to equilibrium. It is particularly useful for studying the robustness of social conventions and norms in dynamic game-theoretic settings.

## Key Features

- **Two-Player Game Engine**: Configurable N×M games with arbitrary payoff matrices for both players
- **Memory-Based Behavior**: Agents remember past joint actions and base decisions on that history
- **Multiple Response Rules**: 
  - Exhaustive best responses (condition on distinct actions observed in history)
  - Expected payoff maximization (empirical frequency-based expectations)
  - Epsilon-greedy stochastic actions for bounded rationality
- **Shock Analysis**: 
  - Temporary payoff matrix changes to represent exogenous disruptions
  - Track pre-shock, post-shock, and recovery-phase frequencies
  - Measure time-to-recovery for each action pair
- **Initial State Modes**: 
  - Random initial histories
  - Balanced distributions
  - Canonical (exhaustive enumeration) distributions
- **Comprehensive Analysis & Visualization**:
  - Pre/post-shock frequency comparisons
  - Recovery time distributions
  - Heatmaps across memory length and noise parameters
  - Recovery rate statistics
  - Individual trajectory plotting
- **Monte Carlo Sensitivity Analysis**: Systematic parameter sweeps with parallel Monte Carlo runs

## Installation

### Prerequisites
- Python 3.11+
- Conda (recommended) or pip

### Using Conda (Recommended)

```bash
conda env create -f environment.yml
conda activate game-sim-env
```

### Using pip

If you prefer pip, install the required packages:

```bash
pip install numpy pandas matplotlib seaborn openpyxl
```

## Project Structure

```
.
├── README.md                      # This file
├── environment.yml                # Conda environment specification
│
├── simulation.py                  # Core simulation engine
│   ├── simulate_two_player_game()          # Basic game simulation
│   ├── simulate_with_shock()               # Game simulation with payoff shocks
│   ├── run_sensitivity_with_shock()        # Monte Carlo sensitivity analysis
│   └── compute_*_frequencies()             # Recovery time calculations
│
├── response_functions.py          # Decision-making strategies
│   ├── exhaustive_epsilon_policy_*()       # Exhaustive best response policies
│   ├── expected_payoff_policy_*()          # Expected payoff maximization policies
│   └── unified_response()                  # Policy selector wrapper
│
├── analysis.py                    # Result aggregation & reporting
│   ├── build_table_norm_epsilon_memory()   # Master results table
│   ├── build_table_norm_shock_pair()       # Shock outcome analysis
│   └── build_table_norm_initial_state_bin()# Initial state sensitivity
│
├── plotting.py                    # Visualization functions
│   ├── plot_pre_post_frequency_overall()   # Summary bar chart
│   ├── plot_recovery_*_allpairs()          # Recovery vs parameters
│   ├── plot_pre_shock_heatmap_grid()       # Heatmaps (2×2 subplots)
│   ├── plot_individual_run()               # Trajectory visualization
│   ├── heatmap_standard_deviation_grid()   # Variability analysis
│   └── generate_all_outputs()              # Full report generation
│
├── utils.py                       # Utility functions
│   ├── get_distinct_actions_from_history() # Extract observation set
│   ├── best_responses_*()                  # Best response calculation
│   ├── epsilon_greedy_choice()             # Stochastic action selection
│   ├── generate_initial_history()          # History initialization
│   ├── generate_all_distributions()        # Canonical distribution enumeration
│   └── compute_initial_distribution()      # Distribution parsing
│
└── examples.py                    # Example usage & parameter templates
```

## Usage

### Basic Example: Single Simulation

```python
from simulation import simulate_two_player_game

# Define a 2×2 game (Prisoner's Dilemma)
row_player_payoffs = [2, 0, 0, 1]    # (C,C), (C,D), (D,C), (D,D)
col_player_payoffs = [1, 0, 0, 2]

# Run one trajectory
traj_df, freq_df = simulate_two_player_game(
    num_rows=2,
    num_cols=2,
    memory_length=3,
    timeperiod=100,
    row_player_payoffs=row_player_payoffs,
    column_player_payoffs=col_player_payoffs,
    epsilon_row=0.05,
    epsilon_col=0.05,
    response_rule='exhaustive',
    random_seed=42
)

print(traj_df.head())        # Time series of actions
print(freq_df)              # Frequency of each joint action
```

### Shock Simulation

```python
from simulation import simulate_with_shock

# Same setup, but with a temporary payoff shock
shock_payoffs_row = [2, 1, 1, 1]
shock_payoffs_col = [1, 1, 1, 2]

traj_df, freq_df = simulate_with_shock(
    num_rows=2,
    num_cols=2,
    memory_length=3,
    timeperiod=100,
    row_player_payoffs=row_player_payoffs,
    column_player_payoffs=col_player_payoffs,
    shock_payoff_row=shock_payoffs_row,
    shock_payoff_col=shock_payoffs_col,
    epsilon_row=0.05,
    epsilon_col=0.05,
    shock_time=48,              # Shock starts at t=48
    shock_duration=4,           # Lasts 4 periods
    response_rule='exhaustive',
    random_seed=42
)
```

### Full Monte Carlo Sensitivity Analysis

```python
from simulation import run_sensitivity, generate_all_outputs
from utils import generate_all_distributions
import pandas as pd

# Parameters
num_rows, num_cols = 2, 2
row_payoffs = [2, 0, 0, 1]
col_payoffs = [1, 0, 0, 2]
shock_payoffs_row = [2, 1, 1, 1]
shock_payoffs_col = [1, 1, 1, 2]

# Sensitivity ranges
memory_lengths = [1, 2, 3, 4, 5]
epsilons = [0, 0.01, 0.05, 0.1, 0.15]
timeperiods = [100]
shock_times = [48]
shock_durations = [4]

# Precompute canonical distributions
canonical_sets = {}
for mem in memory_lengths:
    canonical_sets[mem] = generate_all_distributions(num_rows, num_cols, mem)

# Run main analysis
results_df = run_sensitivity(
    initial_state_mode='canonical',  # or 'random', 'balanced'
    num_rows=num_rows,
    num_cols=num_cols,
    row_player_payoffs=row_payoffs,
    column_player_payoffs=col_payoffs,
    shock_payoff_row=shock_payoffs_row,
    shock_payoff_col=shock_payoffs_col,
    memory_lengths=memory_lengths,
    epsilons=epsilons,
    timeperiods=timeperiods,
    shock_times=shock_times,
    shock_durations=shock_durations,
    canonical_sets=canonical_sets,
    n_runs_per_setting=10,          # Monte Carlo runs per setting
    export_dir='outputs/analysis',
    iter_name='test_run_1',
    random_seed=123,
    response_rule='exhaustive'
)

# Generate all outputs (tables + visualizations)
outputs = generate_all_outputs(
    results_df, 
    output_dir='outputs/figures'
)

# Access generated tables
table1 = outputs['table1']  # Master results table
table2 = outputs['table2']  # Initial state binned analysis
table3 = outputs['table3']  # Shock pair comparison
```

### Customizing Decision Rules

**Exhaustive Best Response Rule** (Default)
```python
# Agents choose an action from the set of best responses to any distinct 
# opponent action observed in history, plus epsilon-greedy exploration
response_rule = 'exhaustive'
```

**Expected Payoff Rule**
```python
# Agents estimate empirical frequency distribution of opponent actions,
# compute expected payoffs for each own action, then pick best responses
# plus epsilon-greedy exploration
response_rule = 'expected'
```

### Output Files

The library generates two types of outputs:

**1. Data Files** (`export_dir`)
- `monte_carlo_results_[iter_name].xlsx` — Main results table (one row per setting, run, and pair)
- `traj_mem*.xlsx` — Individual trajectory files for each run

**2. Analysis Files** (`output_dir`)
- `table_norm_epsilon_memory.xlsx` — Summary statistics by memory length and epsilon
- `table_norm_initial_state_bin.xlsx` — Recovery by initial state distribution
- `table_norm_shock_pair.xlsx` — Results by shock joint action
- PNG visualizations — Heatmaps and line plots

## Configuration Parameters

### Game Setup
| Parameter | Type | Description |
|-----------|------|-------------|
| `num_rows`, `num_cols` | int | Game dimensions (rows = row player actions, cols = column player actions) |
| `row_player_payoffs` | list | Payoff vector for row player (length = rows × cols) |
| `column_player_payoffs` | list | Payoff vector for column player (length = rows × cols) |
| `shock_payoff_row` | list | Payoff vector during shock period for row player |
| `shock_payoff_col` | list | Payoff vector during shock period for column player |

### Behavioral Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `memory_length` | int | — | Number of past joint actions agents remember |
| `epsilon_row`, `epsilon_col` | float | 0.1 | Probability of random action (per player) |
| `response_rule` | str | 'exhaustive' | Decision rule: 'exhaustive' or 'expected' |
| `initial_state_mode` | str | — | How to initialize: 'random', 'balanced', or 'canonical' |

### Simulation Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `timeperiod` | int | Number of periods to simulate |
| `shock_time` | int | Period when shock begins (or None for no shock) |
| `shock_duration` | int | Number of periods shock lasts |
| `n_runs_per_setting` | int | Monte Carlo replications per parameter combination |
| `random_seed` | int | Seed for reproducibility |

## Key Functions Reference

### Core Simulation
- `simulate_two_player_game()` — Run a single game trajectory
- `simulate_with_shock()` — Single trajectory with payoff shock
- `run_sensitivity()` — Full Monte Carlo analysis across parameter grid

### Analysis
- `build_table_norm_epsilon_memory()` — Aggregate by memory and epsilon
- `build_table_norm_shock_pair()` — Compare shock outcomes
- `build_table_norm_initial_state_bin()` — Bin by initial state and compare

### Visualization
- `generate_all_outputs()` — Produces all tables and plots automatically
- `plot_recovery_heatmap_grid()` — 2×2 grid of recovery time heatmaps
- `plot_pre_shock_heatmap_grid()` — 2×2 grid of pre-shock frequency heatmaps
- `plot_individual_run()` — Trajectory visualization with shock window

## Theory Background

This library implements a **memory-based best-response model** of strategic interaction:

1. **History**: Each agent maintains a list of recent joint actions (payoff pairs played)
2. **Play**: At each time step:
   - Extract distinct opponent actions from history
   - Compute best responses (or expected payoffs)
   - Apply epsilon-greedy action selection
3. **Update**: Add the joint action to history (remove oldest if memory exceeded)
4. **Shock**: Optionally change payoff matrices temporarily
5. **Recovery**: Track time until behavior returns to pre-shock norm

**Key metric**: *Recovery time* = periods until a norm's frequency returns to pre-shock level.

## Dependencies

- `numpy` — Numerical computing
- `pandas` — Data manipulation and analysis
- `matplotlib` — Plotting library
- `seaborn` — Statistical visualization
- `openpyxl` — Excel file I/O

All included in `environment.yml`.

## Example Workflow

See [examples.py](examples.py) for a complete working example showing:
1. Parameter setup
2. Canonical distribution precomputation
3. Sensitivity analysis execution
4. Output generation
5. Result visualization

To run the example:
```bash
python examples.py
```

## Output Interpretation

### Pre/Post Shock Frequencies
Shows how frequently each joint action is played before / after the shock. Used to identify which norms are disrupted and which persist.

### Recovery Time
Number of periods until a norm's frequency returns to at least its pre-shock level. Longer recovery = less resilient norms.

### Heatmaps
Visualize how recovery time (or pre-shock frequency) varies across memory length and epsilon parameters. Identify which parameter combinations produce robust vs. fragile norms.

## Notes for Users

- **Random Seed**: Set `random_seed` for reproducibility
- **Memory Length**: Larger memory captures more history but increases state space
- **Epsilon**: Higher epsilon = more randomness = less stable norms
- **Initial State**: 'canonical' requires precomputation but covers the full distribution space
- **Export Path**: Create parent directories in advance if needed; the library will handle subdirectory creation

## License

MIT License

## Citation

If you use this library in your research, please cite it appropriately. Citation format to be added upon publication.
