---
title: "Checking a reciprocity model"
description: "Three checks for reciprocity models in R: errors that unravel tit-for-tat, sensitivity to the continuation probability, and needing repeat partners."
date: "2026-08-07 12:00"
categories: [evolutionary ecology, evolutionary game theory, model diagnostics, R, ecology tutorial]
image: thumbnail.png
image-alt: "Self-payoff of three reciprocal strategies falling as the error rate rises, tit-for-tat dropping fastest"
---
[Tit-for-tat](../direct-reciprocity-and-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.
```{r}
#| label: engine
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)))
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)))
```
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.
```{r}
#| label: forgiveness
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)))
```
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.
```{r}
#| label: exploit
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)))
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)))
```
```{r}
#| label: fig-noise
#| fig-cap: "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."
#| fig-alt: "Three curves falling with error rate; tit-for-tat drops most steeply, the two forgiving strategies decline more gently."
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")
```
## 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.
```{r}
#| label: wcheck
cat(sprintf("reciprocity beats defection only when w > (T-R)/(T-P) = %.2f\n", (Tt-R)/(Tt-P)))
```
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](../the-prisoners-dilemma/). 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](../hamiltons-rule-and-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.
## Related tutorials
- [Checking a reputation model](../checking-a-reputation-model/)
- [Direct reciprocity and tit-for-tat](../direct-reciprocity-and-tit-for-tat/)
- [Strategies in the iterated game](../strategies-in-the-iterated-game/)
- [Hamilton's rule and kin selection](../hamiltons-rule-and-kin-selection/)
## 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)