The Open-Source Prisoner’s Dilemma Tournament

Open Source Game Theory Excercise

Submit your bot →


1. The big idea

In the ordinary one-shot Prisoner’s Dilemma, defection dominates: whatever the other player does, you score higher by defecting. So the only equilibrium is mutual defection \((D,D)\), even though both players would prefer mutual cooperation \((C,C)\). That’s the whole tragedy.

This tournament changes one rule: your program can read your opponent’s program before deciding. Source code is open. This sounds small, but it breaks the classical result. A program can now make its move conditional on what the opponent’s program does — including what the opponent does against it. Once that’s possible, robust mutual cooperation becomes achievable in a single shot, with no repetition and no trust required.

You will write a bot, submit it, and it will play every other student’s bot (and a slate of reference bots) in two tournaments. We’ll rank everyone and crown winners.

The goal is not to maximize your average score. A bot that tops the leaderboard is fine, but it is not the only interesting bot — and often not the most interesting one. What matters is writing a bot that embodies a clear decision-theoretic idea and whose behavior you can predict and explain. A bot that cooperates only with provably-fair opponents, or one that exploits unconditional cooperators, or one that implements a novel variant of PrudentBot — all of these are more valuable to discuss than a bot that accidentally ranks first.

This is a hands-on lab for the core themes of the day:

  • Commitment & transparency. Being legible to your opponent is a strategic asset, not a liability.
  • The twin / Newcomb structure. Playing a copy of yourself is the Twin Prisoner’s Dilemma. Whether you cooperate with your twin is exactly the CDT-vs-EDT-vs-FDT question.
  • Self-reference and its limits. Two bots that each ask “what does the other do against me?” reference each other circularly. Whether that circle resolves — and how — is Löb’s theorem wearing a Halloween costume.

2. The payoff matrix

We use the standard payoffs. You are the row player; your score is the first number.

\[ \begin{array}{c|cc} & \textbf{Opp: C} & \textbf{Opp: D} \\ \hline \textbf{You: C} & (R,R)=(3,3) & (S,T)=(0,5) \\ \textbf{You: D} & (T,S)=(5,0) & (P,P)=(1,1) \end{array} \]

with the defining inequalities

\[ T > R > P > S \qquad\text{and}\qquad 2R > T + S. \]

Concretely: Temptation \(=5\), Reward \(=3\), Punishment \(=1\), Sucker \(=0\). The second inequality (\(2\cdot 3 > 5 + 0\)) is what makes mutual cooperation better for the pair than taking turns exploiting each other — it’s why a tournament that sums points rewards cooperation.


3. Two leagues

You will submit one agent that knows how to play in both leagues. They reward very different things, and seeing your own bot behave differently across them is half the lesson.

League A — Open Source League B — Iterated
Game one-shot repeated, ~100–200 rounds (length hidden)
Your bot sees the opponent’s source / behavior the history of moves so far
Cannot see any history (there is none) the opponent’s source
Classic result it explores program equilibrium, FDT, Löbian cooperation Axelrod reciprocity (Tit-for-Tat & friends)
The surprise cooperation in a single shot cooperation through reciprocity over time

The leagues are scored separately, and we also report a combined ranking. A bot can top one league and finish near the bottom of the other — a strategy that exploits transparency is not the same as one that builds a reputation over time.


4. What you submit

You may write your bot in any form: real Python, pseudocode, or a careful English description of the strategy. Whatever you submit is compiled into a canonical Agent (see §6) by an LLM, and you review and sign off on the compiled version before the tournament runs. If the compiler misread your intent, you fix it then. Treat that sign-off seriously: the compiled artifact is what actually competes.

Your submission has two parts:

  1. The strategy — your bot’s logic for League A (move_oneshot) and League B (move_iterated). If you only describe one league in detail, the compiler will draft a sensible counterpart for the other and ask you to approve it.
  2. A short design rationale — what decision-theoretic idea your bot embodies and how you expect it to behave against the reference bots.

5. How the engine actually resolves circular bots

This is the conceptual heart of the assignment, so read it carefully.

A natural League-A bot is FairBot: cooperate if I can establish that the opponent cooperates with me. Now put two FairBots together. Each one’s move depends on the other’s move, which depends on the first one’s move… forever. If you tried to settle this by simulation — FairBot A runs FairBot B, which runs FairBot A, which… — you’d either loop forever or hit a depth limit and bottom out at defection. Naive simulation gives you \((D,D)\), and that’s wrong relative to what these bots “should” do.

The fix is to stop simulating and start reasoning about provability. FairBot doesn’t cooperate when it simulates cooperation; it cooperates when it can prove cooperation. The relevant fact is Löb’s theorem:

\[ \text{if } \;\vdash\; \big(\Box P \rightarrow P\big)\quad\text{then}\quad \vdash P. \]

Read \(\Box P\) as “\(P\) is provable.” Löb says: if you can prove “were \(P\) provable, \(P\) would be true,” then \(P\) really is provable. For two FairBots, “we cooperate” plays the role of \(P\), and the circularity is exactly the hypothesis Löb needs. The upshot: two FairBots provably cooperate\((C,C)\) — while a FairBot against a bot that simply always defects gets no such proof and so defects, \((D,D)\). The circle resolves, and it resolves the right way, with no infinite loop.

What our engine does. For League A, the engine does not run your bot by recursive simulation. It treats every “what does the opponent do against X?” query as a provability question and computes the fixed point directly — the stable, self-consistent assignment of moves to every pair of bots. This always terminates. Concretely it reproduces:

  • FairBot vs FairBot \(\to (C,C)\)
  • FairBot vs DefectBot \(\to (D,D)\) (not exploited)
  • FairBot vs CooperateBot \(\to (C,C)\) (FairBot “leaves money on the table”)

The one convention you must know. Some bots have no stable answer (they’re defined so that their move contradicts itself — e.g. “cooperate iff the opponent defects against me,” played against a copy). When the fixed point doesn’t settle, the engine applies a no-wishful-thinking rule: if cooperation cannot be established, default to \(D\). You are never assumed to be cooperated with; cooperation has to be earned by a proof. Design accordingly.

Bots that try to predict the opponent by parsing or simulating the raw source string (rather than asking the structured queries in §6) may land in “unresolved → \(D\).” If you want guaranteed, scored, cooperative behavior, express your logic through the query API.


6. The API

Your compiled bot is a Python class with two methods. You only need to understand the interface; you can write your logic in pseudocode and let the compiler produce this.

class Agent:
    name = "YourBotName"

    # ---- League A: one-shot, open source ----
    # `opp` is a handle to the opponent. You do NOT get raw recursion;
    # each query below is resolved by the engine's fixed-point solver.
    def move_oneshot(self, opp) -> Move:
        ...

    # ---- League B: iterated, history only ----
    # my_hist / opp_hist are lists of past Moves (C/D); t is the round index (0-based).
    def move_iterated(self, my_hist, opp_hist, t) -> Move:
        ...

Move is C or D. In move_oneshot, the queries available on opp are:

Query Meaning
opp.move_against(SELF) What does the opponent do against me? (the key mutual-reference query)
opp.move_against(DEFECT_BOT) What does the opponent do against a bot that always defects?
opp.move_against(COOP_BOT) What does the opponent do against a bot that always cooperates?
opp.source The opponent’s canonical source text (for clique-style identity checks)

Each move_against(...) returns C or D. Requirements for League A: move_oneshot must be deterministic and a pure function of its queries (no randomness, no clocks, no hidden state). The compiler enforces this.

Why opp.source is canonical. Everyone’s bot is normalized into the same form before play, so “cooperate iff the opponent’s source equals mine” compares normalized code. Two bots that implement the identical strategy with different variable names will match. This makes identity-based (“clique”) strategies less brittle than raw byte-comparison — but still far more brittle than reasoning about behavior.


7. The reference bots

These are seeded into both leagues. You get their full source in advance (in runtime/agents.py and runtime/iterated_engine.py) so you can reason about and test against them. Knowing how you fare against these is most of the strategy.

League A (one-shot, open source)

# CooperateBot — always cooperates, ignores the opponent.
def move_oneshot(self, opp): return C

# DefectBot — always defects.
def move_oneshot(self, opp): return D

# FairBot — cooperate iff the opponent cooperates with me.
def move_oneshot(self, opp):
    return C if opp.move_against(SELF) == C else D

# PrudentBot — cooperate with fair agents, but punish pushovers.
# (Cooperate iff the opponent cooperates with me AND the opponent
#  is not a sucker, i.e. it defects against DefectBot.)
def move_oneshot(self, opp):
    if opp.move_against(SELF) == C and opp.move_against(DEFECT_BOT) == D:
        return C
    return D

# CliqueBot — cooperate iff the opponent's canonical source equals mine.
def move_oneshot(self, opp):
    return C if opp.source == self.source else D

# RandomBot — cooperates with probability 1/2 (provability queries about it
#             return "unknown", so provers won't cooperate with it).

League B (iterated, history only)

AllC, AllD, Random, TitForTat (cooperate first, then copy the opponent’s last move), GrimTrigger (cooperate until the opponent defects once, then defect forever), GenerousTitForTat (like TfT but forgive a defection ~10% of the time), Pavlov / Win-Stay-Lose-Shift (repeat your last move if it scored well, switch if it scored badly), and TitForTwoTats (only retaliate after two consecutive defections).


8. A worked example: rediscovering prudence

Suppose in League A you start with FairBot. Test it (you’ll have a sandbox):

  • vs DefectBot → you both defect. Good, you’re not exploited. ✔
  • vs FairBot → you both cooperate. ✔
  • vs CooperateBot → you both cooperate. But wait — CooperateBot would let you defect and take the temptation payoff (\(5\) vs \(3\)). FairBot is too nice.

So you patch it: “cooperate only if the opponent cooperates with me and isn’t a pushover.” How do you detect a pushover? Ask opp.move_against(DEFECT_BOT) — a sucker cooperates even with DefectBot. That patch is PrudentBot, and it strictly improves on FairBot: same cooperation with fair agents, but it now exploits unconditional cooperators. Whether you can push this further — and whether being known to be exploitable is ever worth it — is for you to discover.


9. Scoring

  • Payoffs: \(T{=}5,\ R{=}3,\ P{=}1,\ S{=}0\).
  • Round-robin: every bot plays every other bot, including a copy of itself (self-play), plus all reference bots.
  • Rank by total points, not by head-to-head wins. This matters. Tit-for-Tat famously never beats a single opponent — at best it ties — yet it wins tournaments, because it racks up mutual-cooperation points instead of pyrrhic exploitation. Internalize this: the goal is to score points, not to beat your opponent.
  • League B specifics: match length is randomized around ~100–200 rounds and kept hidden (so you can’t defect on “the last round” and unravel cooperation by backward induction).
  • Self-play counts. A bot that defects against its own twin is leaving the single most reliable source of points on the table.

Two rankings are published per league (raw total and combined z-scored overall).


10. Connections to the day

As you design, keep a finger on which idea you’re using:

  • Dominance vs. correlation. Defection dominates in the one-shot game only when your move can’t be correlated with your opponent’s. Open source manufactures correlation. (Newcomb’s problem, EDT vs CDT.)
  • The Twin PD & FDT. Against a copy of yourself, your move and your opponent’s are the same computation. CDT says defect; Functional Decision Theory says recognize the dependence and cooperate, banking \((C,C)\). Your self-play result is a referendum on your decision theory.
  • Commitment as power. A bot that is transparently willing to punish defectors and reward cooperators does better than an opaque one. Legibility beats privacy here — compare Schelling on credible commitment.
  • Löb & the limits of self-reference. The reason two FairBots cooperate (and the reason some bots have no stable behavior at all) is a theorem about provability. Self-reference doesn’t always loop forever — but when it resolves, how it resolves is a real mathematical fact, not a matter of taste.