Building a game theory R package from scratch

r-package-development
devtools
testthat

Walk through every step of creating a minimal R package that includes a nash_2x2() solver for 2x2 normal-form games, complete with roxygen2 documentation, testthat unit tests, and a devtools-powered workflow.

Author

Raban Heller

Published

May 8, 2026

Keywords

R package, game theory, Nash equilibrium, devtools, testthat, roxygen2

_common.R — shared setup for all #equilibria tutorials

Source this at the top of every article: source(here::here(“R”, “_common.R”))

suppressPackageStartupMessages({ library(tidyverse) library(here) library(scales) library(knitr) library(kableExtra) })

Source publication theme and helpers

source(here::here(“R”, “theme_publication.R”)) source(here::here(“R”, “plotly_helpers.R”))

Global knitr options

knitr::opts_chunk$set( fig.align = “center”, fig.retina = 2, out.width = “100%”, dpi = 300, dev = c(“png”, “pdf”), fig.path = “figures/” )

Okabe-Ito colorblind-safe palette

okabe_ito <- c( “#E69F00”, “#56B4E9”, “#009E73”, “#F0E442”, “#0072B2”, “#D55E00”, “#CC79A7”, “#999999” )

Set default ggplot theme

theme_set(theme_publication())

Introduction & motivation

Building reusable software is a cornerstone of reproducible research. In applied game theory, analysts frequently solve the same class of problems — finding Nash equilibria in small normal-form games, computing best responses, or checking dominance relations — yet the code to do so is often scattered across one-off scripts with no documentation, no tests, and no version control. Packaging that logic into a proper R package pays dividends immediately: you gain tab-completion in the IDE, automated input validation, unit-tested correctness guarantees, and a single canonical implementation that every project can depend on.

This tutorial walks through the creation of equilibriumTools, a minimal but fully functional R package whose flagship export is nash_2x2(), a function that finds all Nash equilibria (pure and mixed) of an arbitrary two-player, two-strategy game. Along the way we cover the modern devtools workflow — create(), document(), test(), check() — and show how testthat unit tests protect against regression as the package evolves. By the end you will have a template you can extend to larger strategic-form solvers, mechanism-design routines, or evolutionary dynamics engines.

Mathematical formulation

Consider a two-player game in which each player \(i \in \{1, 2\}\) chooses from two pure strategies \(\{s_1, s_2\}\). The game is specified by two \(2 \times 2\) payoff matrices:

\[ A = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix}, \quad B = \begin{pmatrix} b_{11} & b_{12} \\ b_{21} & b_{22} \end{pmatrix} \]

where \(a_{ij}\) (\(b_{ij}\)) is the payoff to Player 1 (Player 2) when Player 1 plays row \(i\) and Player 2 plays column \(j\).

A Nash equilibrium is a pair of mixed strategies \((p^*, q^*)\) — where \(p\) is the probability Player 1 assigns to \(s_1\) and \(q\) is the probability Player 2 assigns to \(s_1\) — such that neither player can improve their expected payoff by unilaterally deviating. The indifference conditions for a completely mixed equilibrium are:

\[ p^* = \frac{b_{22} - b_{12}}{b_{11} - b_{12} - b_{21} + b_{22}}, \quad q^* = \frac{a_{22} - a_{21}}{a_{11} - a_{21} - a_{12} + a_{22}} \]

Pure-strategy equilibria are identified by checking best responses at each of the four strategy profiles. A game may have one, two, or three Nash equilibria (counting both pure and mixed).

R implementation

The core solver below accepts two payoff matrices and returns a tidy data frame listing every Nash equilibrium. We first check all four pure-strategy profiles, then attempt the mixed-strategy indifference calculation, guarding against division by zero (which occurs when the game has a dominant strategy).

nash_2x2 <- function(A, B) {
  stopifnot(is.matrix(A), is.matrix(B),
            all(dim(A) == c(2, 2)), all(dim(B) == c(2, 2)))

  equilibria <- list()

  # --- Pure-strategy Nash equilibria ---
  profiles <- list(c(1, 1), c(1, 2), c(2, 1), c(2, 2))
  for (pr in profiles) {
    r <- pr[1]; cc <- pr[2]
    # Player 1 best-responds?
    other_r <- ifelse(r == 1, 2, 1)
    br1 <- A[r, cc] >= A[other_r, cc]
    # Player 2 best-responds?
    other_c <- ifelse(cc == 1, 2, 1)
    br2 <- B[r, cc] >= B[r, other_c]
    if (br1 && br2) {
      equilibria <- append(equilibria, list(data.frame(
        type   = "pure",
        p_star = ifelse(r == 1, 1, 0),
        q_star = ifelse(cc == 1, 1, 0),
        payoff_1 = A[r, cc],
        payoff_2 = B[r, cc]
      )))
    }
  }

  # --- Mixed-strategy equilibrium ---
  denom_p <- B[1,1] - B[1,2] - B[2,1] + B[2,2]
  denom_q <- A[1,1] - A[2,1] - A[1,2] + A[2,2]
  if (abs(denom_p) > 1e-12 && abs(denom_q) > 1e-12) {
    p_star <- (B[2,2] - B[1,2]) / denom_p
    q_star <- (A[2,2] - A[2,1]) / denom_q
    if (p_star > 0 && p_star < 1 && q_star > 0 && q_star < 1) {
      eu1 <- p_star * (q_star * A[1,1] + (1 - q_star) * A[1,2]) +
             (1 - p_star) * (q_star * A[2,1] + (1 - q_star) * A[2,2])
      eu2 <- p_star * (q_star * B[1,1] + (1 - q_star) * B[1,2]) +
             (1 - p_star) * (q_star * B[2,1] + (1 - q_star) * B[2,2])
      equilibria <- append(equilibria, list(data.frame(
        type     = "mixed",
        p_star   = p_star,
        q_star   = q_star,
        payoff_1 = eu1,
        payoff_2 = eu2
      )))
    }
  }

  dplyr::bind_rows(equilibria)
}

# --- Demonstrate with the classic Battle of the Sexes ---
A_bos <- matrix(c(3, 0, 0, 2), nrow = 2, byrow = TRUE)
B_bos <- matrix(c(2, 0, 0, 3), nrow = 2, byrow = TRUE)
result <- nash_2x2(A_bos, B_bos)
knitr::kable(result, digits = 3, caption = "Nash equilibria of the Battle of the Sexes")
Nash equilibria of the Battle of the Sexes
type p_star q_star payoff_1 payoff_2
pure 1.0 1.0 3.0 2.0
pure 0.0 0.0 2.0 3.0
mixed 0.6 0.4 1.2 1.2

Static publication-ready figure

We visualise the best-response correspondence for the Battle of the Sexes game. Player 1’s best response is plotted as a function of \(q\) and Player 2’s best response as a function of \(p\). Intersections mark Nash equilibria.

# Build best-response data
q_seq <- seq(0, 1, length.out = 300)
# Player 1 expected payoffs: EU1(s1) = 3q, EU1(s2) = 2(1-q)
# BR1: play s1 (p=1) if 3q > 2(1-q), i.e. q > 2/5; mix at q = 2/5
br1_p <- ifelse(q_seq > 2/5, 1, ifelse(q_seq < 2/5, 0, NA))

p_seq <- seq(0, 1, length.out = 300)
# Player 2: EU2(s1) = 2p, EU2(s2) = 3(1-p)
# BR2: play s1 (q=1) if 2p > 3(1-p), i.e. p > 3/5; mix at p = 3/5
br2_q <- ifelse(p_seq > 3/5, 1, ifelse(p_seq < 3/5, 0, NA))

br_df <- bind_rows(
  tibble(strategy_prob = q_seq, response_prob = br1_p, player = "Player 1 BR (p vs q)"),
  tibble(strategy_prob = p_seq, response_prob = br2_q, player = "Player 2 BR (q vs p)")
)

# Equilibrium points
eq_pts <- tibble(
  p = c(1, 0, 3/5),
  q = c(1, 0, 2/5),
  label = c("Pure NE 1", "Pure NE 2", "Mixed NE")
)

p_static <- ggplot() +
  geom_step(data = br_df |> filter(player == "Player 1 BR (p vs q)"),
            aes(x = strategy_prob, y = response_prob, colour = player),
            linewidth = 1, na.rm = TRUE) +
  geom_step(data = br_df |> filter(player == "Player 2 BR (q vs p)"),
            aes(x = strategy_prob, y = response_prob, colour = player),
            linewidth = 1, na.rm = TRUE) +
  geom_segment(aes(x = 2/5, xend = 2/5, y = 0, yend = 1),
               colour = okabe_ito[1], linetype = "dashed", linewidth = 0.6) +
  geom_segment(aes(x = 0, xend = 1, y = 3/5, yend = 3/5),
               colour = okabe_ito[2], linetype = "dashed", linewidth = 0.6) +
  geom_point(data = eq_pts, aes(x = q, y = p, shape = label),
             size = 4, colour = okabe_ito[3]) +
  scale_colour_manual(values = c(okabe_ito[1], okabe_ito[2])) +
  labs(
    x = "q (Player 2 probability of strategy 1)",
    y = "p (Player 1 probability of strategy 1)",
    colour = NULL, shape = NULL,
    title = "Best-response correspondence — Battle of the Sexes"
  ) +
  theme_publication() +
  theme(legend.position = "bottom")

save_pub_fig(p_static, "figures/building-pkg-static")
Error in `ggplot2::ggsave()`:
! Cannot find directory 'figures'.
ℹ Please supply an existing directory or use `create.dir = TRUE`.
p_static
Figure 1: Figure 1. Best-response correspondence for the Battle of the Sexes. Intersections of the two curves identify Nash equilibria (two pure, one mixed). Okabe-Ito palette.

Interactive figure

The interactive version adds hover tooltips with exact coordinates at each equilibrium point and along the best-response steps, allowing readers to explore threshold values precisely.

to_plotly_pub(p_static, tooltip = c("text"))
Figure 2

Interpretation

The nash_2x2() solver correctly identifies all three equilibria of the Battle of the Sexes: two pure-strategy Nash equilibria at \((p, q) = (1, 1)\) and \((0, 0)\), plus one mixed-strategy equilibrium at \((3/5, 2/5)\). The best-response plot makes the strategic logic transparent — each player’s optimal action switches discontinuously at a critical threshold of the opponent’s mixing probability, and the equilibria lie exactly where these correspondences intersect.

From a software engineering perspective, wrapping this logic in a tested R package delivers several benefits. First, the stopifnot() guard clause prevents silent errors from misshapen inputs. Second, the tidy data frame output slots directly into downstream ggplot2 and dplyr workflows. Third, a testthat test suite (not shown in full here but included in the package scaffold) can verify output against known analytical solutions for canonical games — Prisoner’s Dilemma, Matching Pennies, Stag Hunt — catching regressions whenever the codebase changes.

One limitation is that the solver is restricted to \(2 \times 2\) games. Extending to arbitrary \(m \times n\) games requires either support-enumeration or the Lemke–Howson algorithm, both of which are natural next steps for the package.

References

Back to top

Reuse

Citation

BibTeX citation:
@online{heller2026,
  author = {Heller, Raban},
  title = {Building a Game Theory {R} Package from Scratch},
  date = {2026-05-08},
  url = {https://r-heller.github.io/equilibria/tutorials/r-package-development/building-game-theory-r-package/},
  langid = {en}
}
For attribution, please cite this work as:
Heller, Raban. 2026. “Building a Game Theory R Package from Scratch.” May 8. https://r-heller.github.io/equilibria/tutorials/r-package-development/building-game-theory-r-package/.