Where the Coq lives

| Comments GitHub

An introduction to combinatorial game theory

In this post, I will begin to formalize a small part of combinatorial game theory using Coq. Combinatorial game theory attempts to model sequential, deterministic games between two players, both of which take turns causing the game state to change. It restricts itself to perfect information games, where the current configuration of the game is known to both players. Thus, it can be used to study games such as chess, tic-tac-toe, and go, but not games such as poker or blackjack.
The power of combinatorial game theory comes from abstracting away details that are too specific to each game, such as whether it is played moving pieces on a board, how the pieces can move, etc. It defines a single mathematical object that can represent all games uniformily, allowing us to study general situations that could occur in many kinds of games. In this post, I will present this representation and discuss why it makes sense. I will start with a more intuitive formalization of combinatorial games, and then show how combinatorial game theory applies to all of those.

Defining combinatorial games

In combinatorial games, players are traditionally called left and right.

Inductive player : Type := Left | Right.

Each combinatorial game has a set of positions. In chess, for instance, a position is a chess board with black and white pieces on it. The rules of the game determine which moves are available to each player at a given position. They also describe how the game ends, and which player wins in that case. To simplify our analysis, we will assume that games end when some player must play but has no moves left, in which case the other player wins. This frees us from having to model how matches end separately for each game. We will also consider only finite games, i.e. ones that can't be played indefinitely. Notice that these two assumptions taken together, strictly speaking, rule out many interesting games: chess can end in a draw, something that can't happen in our model.
Translating the above requirements into code results in the following definition:

Inductive combinatorial_game := CombinatorialGame {
  position : Type;
  moves : player -> position -> list position;
  valid_move next current := exists s, In next (moves s current);
  finite_game : well_founded valid_move

To formalize how games are played, we define a predicate Match cg first winner m that represents a match of game cg where player first starts and player winner wins. m is the sequence of positions traversed during the match, from first to last.

Definition other (s : player) : player :=
  match s with
    | Left => Right
    | Right => Left

Inductive Match cg : forall (first winner : player), list (position cg) -> Prop :=
| Match_end : forall pl pos,
                moves cg pl pos = [] ->
                Match cg pl (other pl) [pos]
| Match_move : forall pl winner pos next m,
                 In next (moves cg pl pos) ->
                 Match cg (other pl) winner (next :: m) ->
                 Match cg pl winner (pos :: next :: m).

In the Match_end clause, we check that the current player has no moves left. In Match_move, we check that the current player can make a move and that the match can then proceed with the other positions.

A universal game

We will now define a combinatorial game that is, in a precise sense to be explained later, the most general one. This is what combinatorial game theory uses to study combinatorial games.
The crucial observation is that in the definition of Match we only care about the moves that can be made from a given position, but not about what the positions themselves are. This suggests a definition where each position is just a pair of sets, one for each player's moves. This forms the type game of games.

Inductive game := Game {
  left_moves : list game;
  right_moves : list game

Each position in this game can be pictured as an arbitrarily-branching tree with two sets of children. On each player's turn, they choose one child in their set of moves to be the new position, and lose if they can't choose anything.
The simplest game is zero, where both players have no moves available. It is a game where the first player always loses.

Definition zero : game := Game [] [].

Using zero we can define star, where the first player always wins by making a move to zero.

Definition star : game := Game [zero] [zero].

A small variation gives us one and minus_one, where Left and Right always win, respectively.

Definition one : game := Game [zero] [].
Definition minus_one : game := Game [] [zero].

It should be possible to encapsulate game in a combinatorial_game record. Defining moves is simple, but proving that games are finite requires some additional work. The nested inductive in the definition of game makes Coq generate an induction principle that is too weak to be useful:

     : forall P : game -> Prop,
       (forall left_moves right_moves : list game,
        P {| left_moves := left_moves; right_moves := right_moves |}) ->
       forall g : game, P g
The usual solution is to define a new one by hand that gives us induction hypotheses to use:

Lemma lift_forall :
  forall T (P : T -> Prop),
    (forall t, P t) ->
    forall l, Forall P l.
Proof. induction l; auto. Defined.

Definition game_ind' (P : game -> Prop)
                     (H : forall l r, Forall P l -> Forall P r -> P (Game l r)) :
  forall g : game, P g :=
  fix F (g : game) : P g :=
  match g with
    | Game l r =>
      H l r (lift_forall _ P F l) (lift_forall _ P F r)

Using this principle, we can now prove that games always terminate and define a combinatorial_game for game.

Definition game_as_cg : combinatorial_game.
  refine ({| position := game;
             moves s := if s then left_moves else right_moves |}).
  intros p1.
  induction p1 as [l r IHl IHr] using game_ind'.
  (* ... *)

Game embeddings

I claimed that game is the most general combinatorial game. One way of seeing this is that we lose no information by representing each position in a combinatorial game as the tree of all possible moves, and such a tree can always be encoded as a game. To make this intuition formal, we can define a notion of game embedding between two combinatorial games. This will be a mapping between the positions of each combinatorial game that preserves matches. Thus, if we have an embedding of cg1 into cg2, then we can study cg1 matches by regarding them as cg2 matches.

Definition game_embedding (cg1 cg2 : combinatorial_game)
           (embedding : position cg1 -> position cg2) : Prop :=
  forall first winner (m : list (position cg1)),
    Match cg1 first winner m ->
    Match cg2 first winner (map embedding m).

With this notion of game embedding, combinatorial games form a category. I will now show that every combinatorial game can be embedded in game, making game a terminal object in this category and the most general combinatorial game. In this formulation, it is only a weakly terminal object (i.e., embeddings are not unique), as we are using Coq lists to represent sets.
To embed an arbitrary combinatorial game into game, we can define a function by well-founded recursion over the proof that games are finite. In order to do this, we need a higher-order function map_game that allows us to perform a well-founded recursive call on a list of next moves. map_game acts like map, but passes to its argument function a proof that the element is a valid_move.

Fixpoint map_In {A B} (l : list A) : (forall x, In x l -> B) -> list B :=
  match l with
    | [] => fun _ => []
    | x :: l' => fun f =>
                   f x (or_introl _ eq_refl)
                     :: map_In l' (fun x P => f x (or_intror _ P))

Definition map_game {A} (cg : combinatorial_game)
                    (pos : position cg) (p : player)
                    (f : forall pos', valid_move cg pos' pos -> A) : list A :=
  map_In (moves cg p pos) (fun pos' P => f pos' (ex_intro _ p P)).

Using this function and the Fix combinator in the standard library, we write a generic embedding function embed_in_game. Like a regular fixpoint combinator, Fix takes a function that does a recursive call by applying its argument (here, F). The difference is that this argument must take a proof that shows that the recursive call is valid.
The behavior of embed_in_game is simple: it calls itself recursively for each possible next position, and includes that position in the set of moves of the appropriate player.

Definition embed_in_game cg (pos : position cg) : game :=
  Fix (finite_game cg)
      (fun _ => position game_as_cg)
      (fun pos F =>
         Game (map_game cg pos Left F)
              (map_game cg pos Right F))
Definitions that use Fix can be hard to manipulate directly, so we need to prove some equations that describe the reduction behavior of the function. I've hidden some of the auxiliary lemmas and proofs for clarity; as usual, you can find them in the original .v file.
The proof that we can unfold embed_in_game once uses the Fix_eq lemma in the standard library.

Lemma embed_in_game_eq cg (pos : position cg) :
  embed_in_game cg pos =
  Game (map (embed_in_game cg) (moves cg Left pos))
       (map (embed_in_game cg) (moves cg Right pos)).
  unfold embed_in_game in *.
  rewrite Fix_eq.
  (* ... *)

With this lemma, we can show that moves and embed_in_game commute.

Lemma embed_in_game_moves cg (p : position cg) :
  forall s, moves game_as_cg s (embed_in_game cg p) =
            map (embed_in_game cg) (moves cg s p).
  rewrite embed_in_game_eq.
  destruct s; reflexivity.

We are now ready to state and prove our theorem: every combinatorial game can be embedded in game.

Theorem embed_in_game_correct cg :
  game_embedding cg game_as_cg (embed_in_game cg).
  unfold game_embedding.
  intros first winner m MATCH.
  induction MATCH as [winner p H|s winner p p' m IN MATCH IH];
  simpl; constructor; eauto.
  - rewrite embed_in_game_moves, H. reflexivity.
  - rewrite embed_in_game_moves. auto using in_map.


We've developed the foundations of combinatorial game theory, showing how it can model combinatorial games in a simple yet general way. We haven't explored yet how to use this representation in practice to study games, something I plan to do on future posts.
Update: I'll leave the list of posts in this series here.
comments powered by Disqus