War of Attrition

Two-player waiting game exploring mixed strategies and the option value of waiting.

Level:Intermediate

game-theorywaiting-gamesimpy

  • Probes:contest_length, surplus, cost_variance
FAQ
How are players' quitting times chosen?
Each player draws a quit time from an exponential distribution inversely proportional to their cost of waiting.
How is the winner determined?
Whichever player does not trigger first_quit wins the prize when the other leaves.
How is surplus calculated?
The prize minus the sum of both players' waiting costs over the contest length.
simulation.py

War of Attrition with endogenous stopping

Two players compete for a single prize by waiting each other out. Each draws a private cost of waiting from an exponential distribution and independently chooses when to quit. Whoever quits first forfeits the prize but pays less in waiting cost.

We repeat many rounds to build up a distribution of contest lengths and the surplus captured from the prize. Lower waiting costs translate into longer expected times before quitting, illustrating the real option value of holding out.


from tys import probe, progress


def simulate(cfg: dict):
    """Run repeated wars of attrition and track outcomes."""

    import random
    import simpy

    prize = cfg["prize"]
    mean_cost = cfg["mean_cost"]
    rounds = cfg.get("rounds", 100)
    rng = random.Random(cfg.get("seed"))

    lengths: list[float] = []
    surpluses: list[float] = []

    for i in range(rounds):
        cost_a = rng.expovariate(1 / mean_cost)
        cost_b = rng.expovariate(1 / mean_cost)

        env = simpy.Environment()
        first_quit = env.event()

Each player waits a random time inversely proportional to their cost.

        def player(name: str, cost: float):
            t_quit = rng.expovariate(cost)
            yield env.timeout(t_quit)
            if not first_quit.triggered:
                first_quit.succeed((name, t_quit, cost))

        env.process(player("A", cost_a))
        env.process(player("B", cost_b))
        env.run(until=first_quit)

        loser, t, loser_cost = first_quit.value
        winner_cost = cost_b if loser == "A" else cost_a

        lengths.append(t)
        total_cost = (loser_cost + winner_cost) * t
        surpluses.append(prize - total_cost)

        probe("contest_length", i, t)
        probe("surplus", i, prize - total_cost)
        probe("cost_variance", i, abs(cost_a - cost_b))
        progress(int(100 * (i + 1) / rounds))

    avg_len = sum(lengths) / rounds
    avg_surplus = sum(surpluses) / rounds
    return {
        "avg_length": avg_len,
        "avg_surplus": avg_surplus,
    }


def requirements():
    return {
        "builtin": ["micropip", "pyyaml"],
        "external": ["simpy==4.1.1"],
    }
Default.yaml
prize: 10
mean_cost: 1.0
rounds: 200
seed: 42
Charts (Default)

contest_length

contest_length chart
Samples200 @ 0.00–199.00
Valuesmin 0.00, mean 0.87, median 0.44, max 9.06, σ 1.40

surplus

surplus chart
Samples200 @ 0.00–199.00
Valuesmin 2.29, mean 8.97, median 9.35, max 10.00, σ 1.10

cost_variance

cost_variance chart
Samples200 @ 0.00–199.00
Valuesmin 0.01, mean 1.06, median 0.71, max 8.86, σ 1.11
Final Results (Default)
MetricValue
avg_length0.87
avg_surplus8.97