Skip to content

Encode a new game

This page shows how to write down a symmetric normal-form game so that the definitions of the tutorial, IsSymmetricNash and IsESS, apply to it. The recipe has three steps: a type of actions, a payoff function by cases, and a check that the encoding says what we meant.

A type of actions

We give the actions names with an inductive type, one constructor per action, inside a namespace named after the game:

namespace StagHunt

/-- The two actions in the Stag Hunt. -/
inductive Choice where
  | stag
  | hare
  deriving Repr

open Choice

The namespace matters as soon as a project has more than one game. The Prisoner's Dilemma of the tutorial already defines a top-level payoff, and a second top-level payoff would clash with it. Inside namespace StagHunt the new one is StagHunt.payoff, and the two never meet. The file ends with end StagHunt.

A payoff function by cases

The payoff to the first player is a function of both actions, written as one line per cell of the matrix:

/-- The payoff to the first player: 4 for hunting stag together, nothing for
hunting stag alone, and 3 for hunting hare whatever the other player does. -/
def payoff : Choice → Choice → ℝ
  | stag, stag => 4
  | stag, hare => 0
  | hare, stag => 3
  | hare, hare => 3

Lean checks that every cell is covered, and rejects the definition with a Missing cases error otherwise. Since the game is symmetric, the second player's payoff is payoff with the arguments swapped, and nothing more needs to be written.

Larger games and default cells

With more actions the matrix grows quadratically, and it is often clearer to list the interesting cells and let a final line cover the rest. For Rock, Paper, Scissors, in its own namespace:

/-- The three hands in Rock, Paper, Scissors. -/
inductive Hand where
  | rock
  | paper
  | scissors
  deriving Repr

open Hand

The pattern _, _ matches anything, and cases are tried from the top:

/-- The payoff to the first player: 1 for a win, -1 for a loss, 0 for a draw.
The last line covers every pair not listed above it. -/
def payoff : Hand → Hand → ℝ
  | rock, scissors => 1
  | scissors, paper => 1
  | paper, rock => 1
  | scissors, rock => -1
  | paper, scissors => -1
  | rock, paper => -1
  | _, _ => 0

The cost is that Lean can no longer warn about a forgotten cell, since the last line catches it silently. Use a default line only when the default really is the common case, as the draws are here.

A check that the encoding is right

A typo in a payoff matrix produces a well-formed definition of the wrong game, and Lean will not notice. The cheapest defence is to prove a fact about the game that is already known. The Stag Hunt has two pure symmetric equilibria:

/-- Hunting stag together is a symmetric Nash equilibrium. -/
theorem stag_isSymmetricNash : IsSymmetricNash payoff stag := by
  intro deviation
  cases deviation <;> norm_num [payoff]

/-- So is hunting hare together. -/
theorem hare_isSymmetricNash : IsSymmetricNash payoff hare := by
  intro deviation
  cases deviation <;> norm_num [payoff]

and Rock, Paper, Scissors has none:

/-- No hand is a symmetric Nash equilibrium: against rock, paper does better. -/
theorem not_rock_isSymmetricNash : ¬ IsSymmetricNash payoff rock := by
  intro claim
  have beaten := claim paper
  norm_num [payoff] at beaten

If a check that should succeed fails, suspect the matrix before the proof.

Games that are not symmetric

The definitions in these notes assume a symmetric game, so a single payoff function suffices. A game with different roles, such as Matching Pennies, needs a payoff function for each player, and definitions of equilibrium that compare each player's payoffs separately. Nothing in the recipe above changes, but the tutorial's IsSymmetricNash no longer applies.