A lightweight, pluggable simulation framework for the Beer Distribution Game that enables experimentation with different behavioral profiles to study bullwhip effects and supply chain dynamics.
This project implements a configurable simulation of the famous Beer Distribution Game, allowing you to:
- Experiment with behavioral profiles: Test different decision-making patterns across supply chain roles
- Study bullwhip effects: Observe how small demand changes amplify upstream
- Mix human and AI agents: Combine automated agents with human-in-the-loop decision making
- Analyze supply chain dynamics: Visualize inventory, orders, backlogs, and costs over time
The simulation supports four supply chain roles (Retailer → Wholesaler → Distributor → Factory) with configurable lead times, costs, and demand patterns.
- Python 3.10 or higher
- Git (for cloning)
-
Clone the repository:
git clone <repository-url> cd "Beer Game"
-
Create and activate virtual environment:
# Windows
python -m venv .venv ..venv\Scripts\activate
python -m venv .venv source .venv/bin/activate
3. **Install dependencies**:
```bash
cd profiles-beergame
pip install -r requirements.txt
- Install the package in development mode:
pip install -e .
-
Start Jupyter Lab:
jupyter lab
-
Open the quickstart notebook: Navigate to
notebooks/00_quickstart.ipynband run the cells to see a basic simulation with random agents.
Beer Game/
├── README.md # This file
├── docs/ # Documentation
│ ├── PRD.md # Product Requirements Document
│ ├── PROFILES.md # Behavioral profiles documentation
│ ├── PLAN.md # Development plan
│ └── ARCHITECTURE.md # System architecture
├── profiles-beergame/ # Main Python package
│ ├── src/profiles_beergame/
│ │ ├── interfaces.py # Agent protocol and state definitions
│ │ ├── engine/
│ │ │ └── simulation.py # Core simulation engine
│ │ ├── agents/ # Behavioral agent implementations
│ │ │ ├── random_baseline.py
│ │ │ ├── passive_pipeline.py
│ │ │ └── human_agent.py
│ │ ├── metrics/
│ │ │ └── analytics.py # KPI calculations (bullwhip, costs)
│ │ └── plots/
│ │ └── plotting.py # Visualization utilities
│ ├── requirements.txt # Python dependencies
│ └── pyproject.toml # Package configuration
├── notebooks/ # Jupyter notebooks for experiments
│ ├── 00_quickstart.ipynb # Basic usage example
│ ├── 01_profiles_mixture_template.ipynb
│ └── 02_profile_randomization_demo.ipynb # Profile randomization examples
└── tools/ # Utility scripts
└── extract_profiles.py
from profiles_beergame.engine.simulation import SimulationRunner, SimulationConfig, constant_demand
from profiles_beergame.agents.random_baseline import RandomBaselineAgent
from profiles_beergame.agents.passive_pipeline import PassivePipelineAgent
# Create agents for each role
agents = {
'retailer': RandomBaselineAgent(low=2, high=8, seed=1),
'wholesaler': PassivePipelineAgent(target_inventory=12),
'distributor': PassivePipelineAgent(target_inventory=12),
'factory': RandomBaselineAgent(low=3, high=7, seed=4)
}
# Configure simulation parameters
config = SimulationConfig(
periods=52, # Number of weeks to simulate
order_lead_time=1, # Weeks for orders to reach upstream
shipment_lead_time=2, # Weeks for shipments to reach downstream
initial_inventory=12, # Starting inventory for all roles
holding_cost=0.5, # Cost per unit held in inventory
backlog_cost=1.0, # Cost per unit of unmet demand
random_seed=42 # For reproducible results
)
# Run simulation
runner = SimulationRunner(agents, constant_demand(4), config)
results = runner.run() # Returns pandas DataFrame with detailed logs
print(results.head())Simple stochastic agent for baseline comparisons:
agent = RandomBaselineAgent(low=0, high=10, seed=42)Maintains target inventory levels with minimal changes:
agent = PassivePipelineAgent(target_inventory=12)Over-orders to avoid stockouts, amplifies demand signals:
agent = AggressiveGrowthHackerAgent(amplification_factor=1.5, safety_buffer=3)Risk-averse, prefers low inventory even with occasional stockouts:
agent = ConservativeCustodianAgent(conservation_factor=0.8, max_inventory_target=8)Reactive, emotional decision-making with inconsistent approach:
agent = MyopicFirefighterAgent(emotional_volatility=0.3, panic_threshold=3)Extrapolates recent trends aggressively, misinterprets noise as signals:
agent = SignalChaserAgent(trend_sensitivity=2.0, extrapolation_factor=1.8)Enables human-in-the-loop decision making:
# Interactive input
agent = HumanAgent()
# Or with custom callback
def my_decision_logic(state):
return state.incoming_order + 2 # Simple rule
agent = HumanAgent(prompt_func=my_decision_logic)from profiles_beergame.metrics.analytics import compute_bullwhip, summarize_kpis
from profiles_beergame.plots.plotting import plot_time_series
# Calculate bullwhip effect
bullwhip = compute_bullwhip(results)
print("Bullwhip factors by role:")
print(bullwhip)
# Summarize key performance indicators
kpis = summarize_kpis(results)
print("\nKPI Summary:")
print(kpis)
# Visualize time series
plot_time_series(results)Create custom demand functions:
def step_demand(step_period=20, initial=4, final=8):
def _demand(t):
return final if t >= step_period else initial
return _demand
def seasonal_demand(base=4, amplitude=2, period=12):
import math
def _demand(t):
return int(base + amplitude * math.sin(2 * math.pi * t / period))
return _demand
# Use in simulation
runner = SimulationRunner(agents, step_demand(20, 4, 8), config)The system supports various behavioral profiles based on supply chain psychology research:
- Aggressive Growth-Hacker: Over-orders to avoid stockouts, amplifies demand signals
- Conservative Custodian: Risk-averse, prefers low inventory even with occasional stockouts
- Myopic Firefighter: Reactive decision-making, swings between extremes
- Signal Chaser: Extrapolates recent trends aggressively
- Passive Pipeline: Maintains stable inventory targets with minimal adjustments
- Retailer: Often exhibits "Signal Chaser" behavior, overreacting to sales fluctuations
- Wholesaler/Distributor: Tend toward "Passive Pipeline", simply forwarding orders
- Factory: May act as "Production Smoother", prioritizing stable manufacturing
See docs/PROFILES.md for detailed behavioral descriptions and psychological foundations.
| Parameter | Default | Description |
|---|---|---|
periods |
52 | Number of simulation periods (weeks) |
order_lead_time |
1 | Weeks for orders to reach upstream partner |
shipment_lead_time |
2 | Weeks for shipments to reach downstream |
initial_inventory |
12 | Starting inventory for all roles |
initial_pipeline |
4 | Initial orders in transit |
holding_cost |
0.5 | Cost per unit of inventory per period |
backlog_cost |
1.0 | Cost per unit of unmet demand per period |
random_seed |
42 | Seed for reproducible random behavior |
Each agent receives a RoleState object containing:
period_index: Current simulation weekrole: Agent's position in supply chainincoming_order: Demand received this periodreceived_shipment: Inventory received this periodinventory_on_hand: Current available inventorybacklog: Unmet demand from previous periodspipeline_on_order: Total inventory ordered but not yet receivedlast_placed_order: Previous period's order quantity
Create diverse behavioral combinations automatically:
from profiles_beergame.agents.profile_randomizer import ProfileRandomizer, create_random_agents, create_mixed_scenario
# Create completely random scenario
randomizer = ProfileRandomizer(seed=42)
scenario = randomizer.create_random_scenario()
agents = randomizer.create_agents_dict(scenario)
# Or use quick function
agents = create_random_agents(seed=42)
# Create mixed scenarios with specific profiles
agents = create_mixed_scenario(
retailer_profile="aggressive_growth_hacker",
wholesaler_profile="conservative_custodian",
distributor_profile="passive_pipeline",
factory_profile=None, # Random for factory
seed=123
)
# Run comparative experiments
scenarios = randomizer.create_balanced_scenarios(num_scenarios=10)
for i, scenario in enumerate(scenarios):
print(f"Scenario {i+1}:")
print(randomizer.describe_scenario(scenario))Create your own behavioral profiles by implementing the AgentProtocol:
from profiles_beergame.interfaces import AgentProtocol, RoleState
class MyCustomAgent(AgentProtocol):
def __init__(self, aggressiveness=1.5, smoothing=0.8):
self.aggressiveness = aggressiveness
self.smoothing = smoothing
self.order_history = []
def decide_order(self, state: RoleState) -> int:
# Your decision logic here
base_order = state.incoming_order
# Apply smoothing based on history
if self.order_history:
avg_recent = sum(self.order_history[-3:]) / len(self.order_history[-3:])
base_order = int(self.smoothing * avg_recent + (1-self.smoothing) * base_order)
# Adjust for inventory position
inventory_gap = max(0, 12 - state.inventory_on_hand - state.pipeline_on_order)
order = int(base_order + self.aggressiveness * inventory_gap)
self.order_history.append(order)
return max(0, order)Run multiple scenarios for statistical analysis:
import pandas as pd
def run_experiment(agent_configs, num_runs=10):
results = []
for run in range(num_runs):
agents = {role: agent_class(**config) for role, (agent_class, config) in agent_configs.items()}
runner = SimulationRunner(agents, constant_demand(4), SimulationConfig(random_seed=run))
df = runner.run()
df['run'] = run
results.append(df)
return pd.concat(results, ignore_index=True)
# Example: Compare different agent combinations
configs = {
'retailer': (RandomBaselineAgent, {'low': 2, 'high': 8}),
'wholesaler': (PassivePipelineAgent, {'target_inventory': 12}),
'distributor': (PassivePipelineAgent, {'target_inventory': 12}),
'factory': (RandomBaselineAgent, {'low': 3, 'high': 7})
}
experiment_data = run_experiment(configs, num_runs=20)- Definition: Variance amplification from downstream to upstream
- Calculation:
var(role_orders) / var(retailer_demand) - Interpretation: Values > 1 indicate demand amplification
- Inventory: Average stock levels and variance
- Backlog: Unmet demand frequency and magnitude
- Costs: Holding costs vs. shortage costs trade-off
- Volatility: Standard deviation of order quantities
- Oscillation: Frequency of order direction changes
- Responsiveness: Correlation between demand changes and order adjustments
- Create new agent file in
profiles-beergame/src/profiles_beergame/agents/ - Implement
AgentProtocolinterface - Add unit tests for decision logic
- Update documentation
- Fork the repository
- Create feature branch:
git checkout -b feature/new-profile - Make changes and add tests
- Submit pull request with detailed description
# Run tests (when implemented)
pytest tests/
# Type checking
mypy src/profiles_beergame/
# Code formatting
black src/profiles_beergame/This toolkit is designed for:
- Supply Chain Management courses: Demonstrate bullwhip effect and coordination challenges
- Behavioral Economics research: Study decision-making under uncertainty
- Operations Research: Test inventory policies and optimization strategies
- Game Theory: Analyze multi-agent interactions and information asymmetries
- Profile Comparison: Run same scenario with different agent types, compare bullwhip factors
- Mixed Strategies: Combine human players with AI agents, observe interaction effects
- Parameter Sensitivity: Vary lead times, costs, and demand patterns
- Information Sharing: Modify agents to access upstream/downstream information
- Disruption Response: Introduce supply interruptions or demand shocks
- Original Beer Game (MIT Sloan)
- Sterman, J.D. (1989). Modeling managerial behavior: Misperceptions of feedback in a dynamic decision making experiment
- Lee, H.L., Padmanabhan, V., & Whang, S. (1997). The bullwhip effect in supply chains
[Add your license information here]
For questions, issues, or contributions:
- Create an issue on GitHub
- Check the documentation in the
docs/folder - Review example notebooks in
notebooks/
Happy experimenting with supply chain dynamics! 🍺📈