Checking a reciprocity model

evolutionary ecology
evolutionary game theory
model diagnostics
R
ecology tutorial
Three checks for reciprocity models in R: errors that unravel tit-for-tat, sensitivity to the continuation probability, and needing repeat partners.
Author

Tidy Ecology

Published

2026-08-07

Tit-for-tat makes reciprocity look easy, and that ease is misleading. The clean result rests on players never making mistakes, always meeting again, and always recognising each other. Loosen any of these and cooperation gets harder, sometimes impossible. This post stress-tests a reciprocity model in base R along all three assumptions.

Check 1: mistakes unravel tit-for-tat

Real animals slip: a cooperator occasionally defects by accident, or a signal is misread. Tit-for-tat has no forgiveness, so one error between two tit-for-tat players triggers an endless echo of mutual retaliation. Add an error probability to the payoff engine and watch.

Tt <- 5; R <- 3; P <- 1; S <- 0
payoff_vec <- c(R, S, Tt, P); swap <- c(1, 3, 2, 4)
strat <- function(p, open = 1) list(p = p, open = open)
ALLD <- strat(c(0,0,0,0), 0); TFT <- strat(c(1,0,1,0), 1)
GTFT <- strat(c(1,0.3,1,0.3), 1)          # tit-for-tat that forgives a defection 30% of the time
WSLS <- strat(c(1,0,0,1), 1)              # win-stay, lose-shift (Pavlov)
per_round <- function(A, B, w = 0.9, e = 0) {
  a0 <- A$open*(1-e) + (1-A$open)*e; b0 <- B$open*(1-e) + (1-B$open)*e
  pi0 <- c(a0*b0, a0*(1-b0), (1-a0)*b0, (1-a0)*(1-b0)); M <- matrix(0, 4, 4)
  for (s in 1:4) { ca <- A$p[s]*(1-e) + (1-A$p[s])*e; cb <- B$p[swap[s]]*(1-e) + (1-B$p[swap[s]])*e
    M[s, ] <- c(ca*cb, ca*(1-cb), (1-ca)*cb, (1-ca)*(1-cb)) }
  as.numeric((1-w) * pi0 %*% solve(diag(4) - w*M) %*% payoff_vec)
}
cat(sprintf("no errors:   TFT vs TFT = %.2f\n", per_round(TFT, TFT, e = 0)))
no errors:   TFT vs TFT = 3.00
cat(sprintf("2%% errors:   TFT vs TFT = %.2f  (down %.0f%%)\n",
            per_round(TFT, TFT, e = 0.02), 100*(1 - per_round(TFT, TFT, e = 0.02)/3)))
2% errors:   TFT vs TFT = 2.82  (down 6%)

A 2% error rate drops tit-for-tat’s payoff against itself from the ideal 3.00 to 2.82, a 6% loss, because every accidental defection starts a run of pointless revenge before the two players stumble back into cooperation. The fix is forgiveness. A generous tit-for-tat that overlooks a defection 30% of the time, or win-stay-lose-shift, which returns to cooperation after mutual defection, both recover most of the loss.

cat(sprintf("2%% errors:  GTFT vs GTFT = %.2f   WSLS vs WSLS = %.2f\n",
            per_round(GTFT, GTFT, e = 0.02), per_round(WSLS, WSLS, e = 0.02)))
2% errors:  GTFT vs GTFT = 2.95   WSLS vs WSLS = 2.91

But forgiveness is not free, and this is the subtle part. Generous tit-for-tat still punishes a persistent defector, so it stays safe: a defector invading it earns only 2.52 against its 2.95. Win-stay-lose-shift keeps returning to cooperation even against a relentless cheat, so a defector milks it, earning 3.07 against its 2.91 and invading. The forgiveness that fixes noise can become the naivety that invites exploitation, and a good reciprocal strategy has to thread that needle.

cat(sprintf("under 2%% noise:  ALLD vs GTFT = %.2f (GTFT earns %.2f -> safe)\n", per_round(ALLD, GTFT, e = 0.02), per_round(GTFT, GTFT, e = 0.02)))
under 2% noise:  ALLD vs GTFT = 2.52 (GTFT earns 2.95 -> safe)
cat(sprintf("under 2%% noise:  ALLD vs WSLS = %.2f (WSLS earns %.2f -> exploited)\n", per_round(ALLD, WSLS, e = 0.02), per_round(WSLS, WSLS, e = 0.02)))
under 2% noise:  ALLD vs WSLS = 3.07 (WSLS earns 2.91 -> exploited)
library(ggplot2)
es <- seq(0, 0.1, 0.005)
d <- rbind(data.frame(e = es, pay = sapply(es, function(e) per_round(TFT, TFT, e = e)), s = "tit-for-tat"),
           data.frame(e = es, pay = sapply(es, function(e) per_round(GTFT, GTFT, e = e)), s = "generous TFT"),
           data.frame(e = es, pay = sapply(es, function(e) per_round(WSLS, WSLS, e = e)), s = "win-stay-lose-shift"))
ggplot(d, aes(e, pay, colour = s)) +
  geom_line(linewidth = 0.9) +
  scale_colour_manual(values = c("tit-for-tat" = "#b5534e", "generous TFT" = "#275139", "win-stay-lose-shift" = "#c9b458")) +
  labs(x = "Error rate", y = "Payoff against own type", colour = NULL) +
  theme_minimal(base_size = 12) + theme(legend.position = "top")
Three curves falling with error rate; tit-for-tat drops most steeply, the two forgiving strategies decline more gently.
Figure 1: Self-payoff of three reciprocal strategies as the error rate rises. Unforgiving tit-for-tat degrades fastest; generous tit-for-tat and win-stay-lose-shift hold up better by returning to cooperation after an accidental defection.

Check 2: the continuation probability decides everything

The whole model turns on how likely the same two players are to meet again. Recompute the threshold that reciprocity needs.

cat(sprintf("reciprocity beats defection only when w > (T-R)/(T-P) = %.2f\n", (Tt-R)/(Tt-P)))
reciprocity beats defection only when w > (T-R)/(T-P) = 0.50

Below a continuation probability of 0.5, tit-for-tat cannot resist defection no matter how the tournament is set up: the future is too short to make retaliation bite. A reciprocity result reported without stating the assumed continuation probability is not interpretable, because the same strategies flip from winning to losing across that one number. It is the sensitivity that matters most, and the one most often left unstated.

Check 3: reciprocity needs repeated, recognised partners

The mechanism assumes players meet the same individual repeatedly and remember what happened. Strip that away and the model does not merely weaken; it stops applying. In a large, well-mixed population where each encounter is with a stranger, the effective continuation probability is near zero, retaliation can never land on the individual that cheated, and defection is dominant exactly as in the one-shot game. Direct reciprocity therefore predicts cooperation only in species with stable groups, individual recognition, and memory. Where those are absent but partners are relatives, cooperation still occurs, but through kin selection rather than reciprocity. Getting the mechanism right matters: the two make different predictions about who helps whom, and only one of them needs a good memory.

The assumption underneath

Every check comes back to the same point: reciprocity is a statement about a particular kind of interaction, repeated, recognised, and remembered, with payoffs and an error rate that the model treats as known. The elegant threshold and the tidy tournament are only as good as those inputs, and in the field the continuation probability and the error rate are rarely measured. Treat a reciprocity model as a hypothesis about the conditions cooperation needs, report how its verdict moves with w and the error rate, and it stays honest.

References

Nowak MA, Sigmund K 1993. Nature 364(6432):56-58 (10.1038/364056a0)

Nowak MA, Sigmund K 1992. Nature 355(6357):250-253 (10.1038/355250a0)

Nowak MA 2006. Science 314(5805):1560-1563 (10.1126/science.1133755)

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.