This vignette illustrates how you can specify dynamic games with the RelationalContracts package. We start with simple examples and move to more complex use cases.

In particular, sections 7-9 give advice on how to effectively specify state transitions for complex game with multidimensional state spaces.

1. Specifying a simple repeated game

Consider a simple two player repeated game. Each player \(i\) chooses an effort level \(e_i\) on a grid between 0 and 1 that directly benefits the other player. Effort involves for player \(i\) cost of \(\frac 1 2 e_i\) and grants the other player a benefit of \(e_i\):

library(RelationalContracts)
effort.seq = seq(0,1,by=0.1)
g = rel_game("Mutual Gift Game") %>%
  rel_state("x0",
    # Both players can pick effort
    # on a grid between 0 and 1
    A1 = list(e1=effort.seq),
    A2 = list(e2=effort.seq),

    # Stage game payoffs
    pi1 = e2 - 0.5*e1^2,
    pi2 = e1 - 0.5*e2^2
  )

# Solve SPE
g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab)
## # A tibble: 1 x 2
##   x     ae.lab   
##   <chr> <chr>    
## 1 x0    0.6 | 0.6

The function rel_game specifies an empty game object and rel_state specifies a state. A repeated game has just a single state, but one can also specify more general stochastic games with several states.

The parameters A1 and A2 specify the action spaces of each player. You have to provide a named list of actions for each player, here we have the actions e1 for player 1 and e2 for player 2. Note that you must choose different names for each player’s actions, i.e. you could not call both actions just e.

The parameters pi1 and pi2 specify payoffs for both players. Payoffs will be lazily evaluated later on the corresponding action space. This means they can depend on the chosen action or parameters specified for the game or state.

2. A game with 2 states and transitions

Consider now the following game with two states:

library(RelationalContracts)
effort.seq = seq(0,1,by=0.1)
vul = 1
g = rel_game() %>%
  rel_state("not_vul",
    A1 = list(e1=effort.seq, make_vul = c(FALSE, TRUE)),
    A2 = list(e2=effort.seq),
    pi1 = e2 - 0.5*e1^2,
    pi2 = e1 - 0.5*e2^2
  ) %>%
  rel_state("vul",
    A1 = list(e1=effort.seq),
    A2 = list(e2=c(-vul,effort.seq)),
    pi1 = e2 - 0.5*e1^2,
    pi2 = e1 - 0.5*pmax(e2,0)^2
  ) %>%
  rel_transition("not_vul","vul", make_vul=TRUE,prob=1) %>%
  rel_transition("vul","not_vul", prob = 0.05) %>%
  rel_compile()

# Solve SP
# 
g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab)
## # A tibble: 2 x 2
##   x       ae.lab     
##   <chr>   <chr>      
## 1 not_vul 0.6 1 | 0.6
## 2 vul     0.9 | 0.9

We now have two states. The game starts in state “not_vul”. Here player 1 has a two dimensional actions space A1. In addition to choosing effort levels, player 1 can choose via make_vul whether to make herself vulnerable or not. In the state “vul”, player 2 can choose negative effort that harms player 1. We specify the state transitions via the function rel_transition.

The command rel_transition("not_vul","vul",make_vul=TRUE,prob=1) means that there is a transition from state “not_vul” to state “vul” with probability prob=1 if the action make_vul=TRUE was chosen. The effort levels do not affect this transition probability.

The line rel_transition("vul","not_vul", prob = 0.05) means that there is a transition from state “vul” to state “not_vul” with probability of 5%, irrespective of which actions have been chosen.

For cases in which no explicit transition probability is specified it is assumed that play remains in the current state. I.e. if player 1 chooses make_vul = FALSE in stat “not_vul” the game remains in state “not_vul” even though we have not explicitly specified this.

3. Specifying Multiple States at Once

Consider the following game specification that has 6 different states determined by increasing vulnerability of player 1:

effort.seq = seq(0,1,by=0.1)
g = rel_game() %>%
  rel_states(x = 0:5,
    A1 = list(e1=effort.seq, inc_vul = c(FALSE, TRUE)),
    A2 = list(e2=c(-1,effort.seq)),
    pi1 = ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 = e1 - 0.5*pmax(e2,0)^2
  ) %>%
  rel_transition(xs = 0:4,xd=1:5, inc_vul=TRUE,prob=1)
  

# Solve SPE
g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab)
## # A tibble: 6 x 2
##       x ae.lab     
##   <int> <chr>      
## 1     0 0.6 1 | 0.6
## 2     1 0.7 1 | 0.7
## 3     2 0.7 1 | 0.8
## 4     3 0.8 1 | 0.8
## 5     4 0.8 1 | 0.9
## 6     5 0.9 0 | 0.9

The function rel_states is just a synonym for rel_state and can be used to specify several states simultaneously. A state x is here identified by an integer between 0 and 5 that relates to player 1’s vulnerability.

Note that the formula for pi1 refers to the current state x.

The function rel_transition allows to specify multiple source states xs and multiple destination states xd. Here choosing inc_vul=TRUE increases the state number, as long as we have not reached yet state x=5.

The specification above specifies the same action space for all states, even though in state x=5 the action inc_vul has no effect.

One way to specify different action spaces for different subsets of states is to add multiple rel_states commands as in the example below:

effort.seq = seq(0,1,by=0.1)
g = rel_game() %>%
  rel_states(x = 0:4,
    A1 = list(e1=effort.seq, inc_vul = c(FALSE, TRUE)),
    A2 = list(e2=c(-1,effort.seq)),
    pi1 =  ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 =  e1 - 0.5*pmax(e2,0)^2
  ) %>%
  # Specify state x=5 separately without inc_vul action
  rel_states(x = 5,
    A1 = list(e1=effort.seq),
    A2 = list(e2=c(-1,effort.seq)),
    pi1 = ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 = e1 - 0.5*pmax(e2,0)^2
  ) %>%
  rel_transition(xs = 0:4,xd=1:5, inc_vul=TRUE,prob=1)

4. Specify action spaces via A.fun instead of A1 and A2

If you want to specify several states with different action spaces in a single call to rel_states you should use the argument A.fun instead of A1 and A2. Below is an example:

effort.seq = seq(0,1,by=0.1)

# Function that specifies action spaces
A.fun = function(x,...) {
  list(
    A1 = list(e1=effort.seq, inc_vul = c(FALSE, if (x<5) TRUE)),
    A2 = list(e2=c(if (x>0) -1,effort.seq))
  )
}

g = rel_game() %>%
  rel_states(x = 0:5,
    A.fun = A.fun,
    pi1 = ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 = e1 - 0.5*pmax(e2,0)^2
  ) %>%
  rel_transition(xs = 0:4,xd=1:5, inc_vul=TRUE,prob=1)
  

# Solve SPE
g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab)
## # A tibble: 6 x 2
##       x ae.lab     
##   <int> <chr>      
## 1     0 0.6 1 | 0.6
## 2     1 0.7 1 | 0.7
## 3     2 0.7 1 | 0.8
## 4     3 0.8 1 | 0.8
## 5     4 0.8 1 | 0.9
## 6     5 0.9 0 | 0.9

The function A.fun will be called separately for each state x and should return a list with elements A1 and A2 describing the action spaces of both players for the current state.

5. Specify payoffs via pi.fun instead of pi1 and pi2

The argument pi.fun of rel_states allows to specify payoffs via a function instead of providing formulas to the arguments pi1 and pi2. Here is an example:

effort.seq = seq(0,1,by=0.1)
pi.fun = function(ax.df, ...) {
  mutate(ax.df,
    pi1 = ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 = e1 - 0.5*pmax(e2,0)^2
  )
}

g = rel_game() %>%
  rel_states(x = 0:5,
    A1 = list(e1=effort.seq, inc_vul = c(FALSE, TRUE)),
    A2 = list(e2=c(-1,effort.seq)),
    pi.fun = pi.fun
  ) %>%
  rel_transition(xs = 0:4,xd=1:5, inc_vul=TRUE,prob=1)

# Solve SPE
g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab)
## # A tibble: 6 x 2
##       x ae.lab     
##   <int> <chr>      
## 1     0 0.6 1 | 0.6
## 2     1 0.7 1 | 0.7
## 3     2 0.7 1 | 0.8
## 4     3 0.8 1 | 0.8
## 5     4 0.8 1 | 0.9
## 6     5 0.9 0 | 0.9

While A.fun will be called separately for each state x, the function pi.fun will be called only once with an argument ax.df (and possible some parameters specified in the game as explained in Section 11). ax.df is a tibble that contains one row for every action profile in every state. In our example, its head and tail looks as follows:

head(ax.df,3)

# A tibble: 1,584 x 5
       x    .a    e1 inc_vul    e2
   <int> <int> <dbl> <lgl>   <dbl>
 1     0     1     0 FALSE    -1  
 2     0     2     0 FALSE     0  
 3     0     3     0 FALSE     0.1

tail(ax.df,3)

# A tibble: 3 x 5
      x    .a    e1 inc_vul    e2
  <int> <int> <dbl> <lgl>   <dbl>
1     5   262     1 TRUE      0.8
2     5   263     1 TRUE      0.9
3     5   264     1 TRUE      1

The column x specifies a state, the column .a will probably not be used, it is just a unique index of the action profile number. The other columns correspond to the action names. (If some action is not present in some state, it is filled with NA)

Your function basically should add the columns pi1 and pi2 to each row of this data frame. You can drop some columns, but should keep x and not change the order of rows.

6. Specify state transitions via trans.fun

As an alternative to specifying state transitions with one or several calls to rel_transition, one can use the argument trans.fun in the call to rel_states. Here is an example:

trans.fun = function(ax.df, ...) {
  tibble(xs=0:4,xd=1:5, inc_vul=TRUE, prob=1)
}
g = rel_game() %>%
  rel_states(x = 0:5,
    A1 = list(e1=effort.seq, inc_vul = c(FALSE, TRUE)),
    A2 = list(e2=c(-1,effort.seq)),
    pi1 =  ifelse(e2 != -1,e2, -x/5) - 0.5*e1^2,
    pi2 =  e1 - 0.5*pmax(e2,0)^2,
    trans.fun = trans.fun
  )

trans.fun is a function that takes the same arguments as explained in the subsection about pi.fun above. It returns a data frame whose columns are essentially the same as the arguments of rel_transition. Each row specifies a transition from the state in column xs to the state in column xd with probability in column prob given that the actions take the values specified in additional columns.

You may add columns for all actions and set a value of NA if this action is not relevant in a particular row. So we could also have specified the state transitions as

trans.fun = function(ax.df, ...) {
  tibble(xs=0:4,xd=1:5, inc_vul=TRUE,e1=NA, e2=NA, prob=1)
}

In the current example, there is little value of using the trans.fun argument instead of rel_transition. Yet, in more complex games with multidimensional state spaces and complex state transitions trans.fun can be more tractable. We will provide examples below.

7. State Space with multiple dimensions and helper variables: A Cournot Duopoly with Capacity Investments

The following examples will be variants of a dynamic Cournot duopoly with capacity investments. A state x is characterized by a tuple (x1,x2) that describes the production capacities of each player. In each period players simultaneously choose integer outputs q1 and q2 between 0 and their capacity and decide whether or not to invest into new capacity. A player can invest until a maximum capacity x.max has been reached.

Product market prices in a period are given by a-q1-q2 where a is an exogenous parameter.

The following code specifies such a Cournot duopoly:

# Exogenous paramaters
x.max = 5
a = 8
i.cost = 1

# State matrix
x.df = tidyr::expand_grid(x1=0:x.max, x2=0:x.max) %>%
  mutate(x=paste0(x1,"_",x2))

# Action space
A.fun = function(x1,x2,...) {
  list(
    A1=list(q1 = 0:x1, i1=c(0, if (x1 < x.max) 1)),
    A2=list(q2 = 0:x2, i2=c(0, if (x2 < x.max) 1))
  )
}

# State transitions
trans.fun = function(ax.df,...) {
  restore.point("trans.fun")
  ax.df %>%
    select(x,x1,x2,i1,i2) %>%
    unique() %>%
    mutate(
      new_x1 = pmin(x1+i1,x.max),
      new_x2 = pmin(x2+i2,x.max),
      xd = paste0(new_x1,"_",new_x2),
      xs=x,
      prob = 1
    )
}

g = rel_game("Cournot with Investment") %>%
  rel_states(x=x.df,
    A.fun=A.fun,
    pi1 = (a-(q1+q2))*q1 - i.cost*i1,
    pi2 = (a-(q1+q2))*q2 - i.cost*i2,
    trans.fun=trans.fun
  )

# g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab,U)

First note that in the call to rel_states, the argument x is not a vector of state names, but instead the data frame x.df specified further above. The data frame has the columns x1 and x2 that provide additional information about each state, namely the capacities of each player. The additional column x uniquely identifies each state as a label that just pastes x1 and x2 together.

The additional state variables x1 and x2can be used in the formula’s for pi1 and pi2, are passed as additional arguments to A.fun, and are additional columns in the ax.df argument passed to trans.fun. You can add as many columns to such a state data frame as you like. The only requirement is that there is a column x with unique values in each row.

The specification of A.fun is pretty straightforward. Note how the action space depends on the current capacity of each player.

More complex is the specification of state transitions in trans.fun. In general it is not easy to specify state transitions correctly on a first trial. I recommend to use debugging via restore points (or some other debugging approach) to build a correct trans.fun. Let me explain, step by step, what we have done.

We start with ax.df which contains one row for each action profile in each state. It looks as follows

> ax.df
# A tibble: 1,296 x 8
      x1    x2 x        .a    q1    i1    q2    i2
   <int> <int> <chr> <int> <int> <dbl> <int> <dbl>
 1     0     0 0_0       1     0     0     0     0
 2     0     0 0_0       2     0     0     0     1
 3     0     0 0_0       3     0     1     0     0
 4     0     0 0_0       4     0     1     0     1
 5     0     1 0_1       1     0     0     0     0
 6     0     1 0_1       2     0     0     1     0
 7     0     1 0_1       3     0     0     0     1
 8     0     1 0_1       4     0     0     1     1
 9     0     1 0_1       5     0     1     0     0
10     0     1 0_1       6     0     1     1     0
# ... with 1,285 more rows

Relevant to compute state transitions are only the investment actions and the state variables. We thus select the relevant columns and reduce to unique rows. We get:

ax.df %>%
  select(x,x1,x2,i1,i2) %>%
  unique()

# A tibble: 121 x 5
   x        x1    x2    i1    i2
   <chr> <int> <int> <dbl> <dbl>
 1 0_0       0     0     0     0
 2 0_0       0     0     0     1
 3 0_0       0     0     1     0
 4 0_0       0     0     1     1
 5 0_1       0     1     0     0
 6 0_1       0     1     0     1
# ... with 115 more rows

Starting with this data frame, we can easily compute the new capacities of both players by adding their investments to the current capacities. The new state xd is then simply created by pasting the new capacities together. We also have to add the transition probability of prob=1 since state transitions are deterministic and set the source state column xs just to the original state x.

8. Specifying state transitions using the helper function irv_joint_dist: Cournot duopoly where investments are not always successful

Consider now a variant of our previous game in which an investment leads to a successful capacity expansion only with an exogenous success probability sp.

Specifying state transitions becomes more complicated now. Assume both players invest, then there are 4 different possible outcomes: no investment is successful, only player 1’s investment is successful, only player 2’s investment is successful, or both investments are successful.

Relational Contracts has a helper function irv_joint_dist that allows to compute joint distributions of independent discrete random variables using a format suitable to specify state transitions. Consider the following example.

# success probability of investment
sp = 0.7

# An example row for which we want
# to compute transition probabilities
df = tibble(x="0_0",x1=0,x2=0,i1=1,i2=1)

# Compute joint distribution of independent
# random varibles
irv_joint_dist(df,
  irv("new_x1",default=x1,
    irv_val(val=x1+1,prob=i1*sp)
  ),
  irv("new_x2",default=x2,
    irv_val(val=x2+1,prob=i2*sp)
  )
)
## # A tibble: 4 x 8
##   x        x1    x2    i1    i2  prob new_x1 new_x2
##   <chr> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl>
## 1 0_0       0     0     1     1 0.490      1      1
## 2 0_0       0     0     1     1 0.21       0      1
## 3 0_0       0     0     1     1 0.21       1      0
## 4 0_0       0     0     1     1 0.09       0      0

Inside the call to irv_joint_dist we specify with irv two discrete valued random variables new_x1 and new_x2 that shall be independently distributed from each other. Inside irv we call irv_val to specify values that the variable can take and their realiziation probabilities. These values and probabilities are computed using the columns of the data frame df passed to irv_joint_dist using lazy evaluation.

For convenience, we can also set a default value for each random variables that is realized with probability 1 minus the sum of probabilities of all explicitly specified probabilities via irv_val. E.g. without default values, we could have equivalently specified new_x1 by

  irv("new_x1",
    irv_val(val=x1+1,prob=i1*sp),
    irv_val(val=x1, prob=1-(i1*sp))
  )

For each row of the passed data frame df, irv_joint_dist creates several rows: one for each possible outcome combination of the specified independent random variables. Since df only has a single row and there are 4 combinations of new_x1 and new_x2, we get a data frame with 4 rows. The column prob contains the probability of the particular outcome combination.

If df contains multiple rows, these computations will be performed separately for each row and the resulting rows are combined together. Consider this example where in one row player 1 invests and in the other not:

# success probability of investment
sp = 0.7

# Two example rows for which we want
# to compute transition probabiluties
df = tibble(x="0_0",x1=0,x2=0,i1=c(0,1),i2=1)
df
## # A tibble: 2 x 5
##   x        x1    x2    i1    i2
##   <chr> <dbl> <dbl> <dbl> <dbl>
## 1 0_0       0     0     0     1
## 2 0_0       0     0     1     1
# Compute joint distribution of independent
# random varibles
irv_joint_dist(df,
  irv("new_x1",default=x1,
    irv_val(val=x1+1,prob=i1*sp)
  ),
  irv("new_x2",default=x2,
    irv_val(val=x2+1,prob=i2*sp)
  )
)
## # A tibble: 6 x 8
##   x        x1    x2    i1    i2  prob new_x1 new_x2
##   <chr> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl>
## 1 0_0       0     0     1     1 0.490      1      1
## 2 0_0       0     0     0     1 0.7        0      1
## 3 0_0       0     0     1     1 0.21       0      1
## 4 0_0       0     0     1     1 0.21       1      0
## 5 0_0       0     0     0     1 0.3        0      0
## 6 0_0       0     0     1     1 0.09       0      0

The result has now 2+4=6 rows. If player 1 does not invest (i1==0), the variable new_x1 always is 0 and there are only two outcomes.

Note that the order of the resulting rows is not very intutive. Internally irv_joint_dist tries to use vectorization for fast computation that sorts the rows in a peculiar way and for time reasons there is no unneccessary rearrangement afterwards.

Below is a specification of the transition function for our Cournot game using irv_joint_dist:

sp = 0.7
trans.fun = function(ax.df,...) {
  restore.point("trans.fun")
  ax.df %>%
    select(x,x1,x2,i1,i2) %>%
    unique() %>%
    irv_joint_dist(df,
      irv("new_x1",default=x1,
        irv_val(val=x1+1,prob=i1*sp)
      ),
      irv("new_x2",default=x2,
        irv_val(val=x2+1,prob=i2*sp)
      )
    ) %>%    
    mutate(
      xd = paste0(new_x1,"_",new_x2),
      xs=x
    )
}

This code may look complicated, but specifying transition probabilities correctly is typically the most difficult task in a game specificiation. And while it requires some time to learn, in my experience, irv_joint_dist is often quite a useful tool for this task.

9. Advanced use of irv_joint_dist: Cournot duopoly where investments are not always successful and capacity may depreciate

Consider now a variant in which existing capacity can also depricate with positive depreciation probability dp.

The following code specifies the transition function:

trans.fun = function(ax.df,...) {
  restore.point("trans.fun")
  ax.df %>%
    select(x,x1,x2,i1,i2) %>%
    unique() %>%
    irv_joint_dist(
      irv("add_x1",default=0,
        irv_val(1,prob=i1*sp)
      ),
      irv("add_x2",default=0,
        irv_val(1,prob=i2*sp)
      ),
      irv("rem_x1",default=0,
        irv_val(1,prob=(x1>0)*dp)
      ),
      irv("rem_x2",default=0,
        irv_val(1,prob=(x2>0)*dp)
      )
    ) %>%
    mutate(
      new_x1 = x1+add_x1 - rem_x1,
      new_x2 = x2+add_x2 - rem_x2,
      xd = paste0(new_x1,"_",new_x2),
      xs=x
    ) %>%
    group_by(xs,xd,i1,i2) %>%
    summarize(prob=sum(prob))
}

We now specify 4 independent random variables. Each new capacity now depends on two random variables describing whether or nor investment successful and whether or not there is depreciation.

To understand why the final group_by and summarize is neccessary let us take a look at the computation before for a single intial row:

sp = 0.7
dp = 0.1

tibble(x="1_0",x1=1,x2=0,i1=1,i2=0) %>%
  select(x,x1,x2,i1,i2) %>%
  unique() %>%
  irv_joint_dist(
    irv("add_x1",default=0,
      irv_val(1,prob=i1*sp)
    ),
    irv("add_x2",default=0,
      irv_val(1,prob=i2*sp)
    ),
    irv("rem_x1",default=0,
      irv_val(1,prob=(x1>0)*dp)
    ),
    irv("rem_x2",default=0,
      irv_val(1,prob=(x2>0)*dp)
    )
  ) %>%
  mutate(
    new_x1 = x1+add_x1 - rem_x1,
    new_x2 = x2+add_x2 - rem_x2,
    xd = paste0(new_x1,"_",new_x2),
    xs=x
  ) %>%
  select(xs,xd,i1,i2,prob,add_x1,rem_x1)
## # A tibble: 4 x 7
##   xs    xd       i1    i2   prob add_x1 rem_x1
##   <chr> <chr> <dbl> <dbl>  <dbl>  <dbl>  <dbl>
## 1 1_0   1_0       1     0 0.0700      1      1
## 2 1_0   0_0       1     0 0.03        0      1
## 3 1_0   2_0       1     0 0.63        1      0
## 4 1_0   1_0       1     0 0.27        0      0

We see that both the first and last row transist to the same state. In the first row both investment and depreciation for player 1 happens, and in the last row neither one happens. Both yield the same net result: no change of capacity. The final group_by and summarize command inside trans.fun add up the probabilities of such rows with the same outcomes.

10. Separating static and dynamic actions

Consider the following variant of the dynamic Cournot duopoly, where we have splitted the “static” actions q1 and q2 which don’t affect the state transitions from the “dynamic” actions i1 and i2:

x.max = 5
# State matrix
x.df = tidyr::expand_grid(x1=0:x.max, x2=0:x.max) %>%
  mutate(x=paste0(x1,"_",x2))

# static action space
static.A.fun = function(x1,x2,...) {
  list(
    A1=list(q1 = 0:x1),
    A2=list(q2 = 0:x2)
  )
}

# dynamic action space 
A.fun = function(x1,x2,x.max,...) {
  list(
    A1=list(i1=c(0, if (x1 < x.max) 1)),
    A2=list(i2=c(0, if (x2 < x.max) 1))
  )
}

# State transitions: deterministic investments
trans.fun = function(ax.df,x.max,...) {
  restore.point("trans.fun")
  ax.df %>%
    select(x,x1,x2,i1,i2) %>%
    unique() %>%
    mutate(
      new_x1 = pmin(x1+i1,x.max),
      new_x2 = pmin(x2+i2,x.max),
      xd = paste0(new_x1,"_",new_x2),
      xs=x,
      prob = 1
    )
}

g = rel_game("Cournot with Investment") %>%
  rel_param(a = 10,i.cost = 1.5,x.max=x.max) %>%  
  rel_states(x=x.df,
    A.fun=A.fun,
    static.A.fun = static.A.fun,
    static.pi1 = (a-(q1+q2))*q1,
    static.pi2 = (a-(q1+q2))*q2,
    pi1 = - i.cost*i1,
    pi2 = - i.cost*i2,
    trans.fun=trans.fun
  )

g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab,U)
##      x        ae.lab      U
## 1  0_0 0 | 0 | 1 | 1  2.835
## 2  0_1 0 | 1 | 1 | 1 10.500
## 3  0_2 0 | 2 | 1 | 0 16.450
## 4  0_3 0 | 3 | 1 | 0 20.850
## 5  0_4 0 | 4 | 0 | 0 24.000
## 6  0_5 0 | 5 | 0 | 0 25.000
## 7  1_0 1 | 0 | 1 | 1 10.500
## 8  1_1 1 | 1 | 0 | 1 16.450
## 9  1_2 1 | 2 | 0 | 0 21.000
## 10 1_3 1 | 3 | 0 | 0 24.000
## 11 1_4 1 | 4 | 0 | 0 25.000
## 12 1_5 1 | 4 | 0 | 0 25.000
## 13 2_0 2 | 0 | 0 | 1 16.450
## 14 2_1 2 | 1 | 0 | 0 21.000
## 15 2_2 2 | 2 | 0 | 0 24.000
## 16 2_3 2 | 3 | 0 | 0 25.000
## 17 2_4 2 | 3 | 0 | 0 25.000
## 18 2_5 2 | 3 | 0 | 0 25.000
## 19 3_0 3 | 0 | 0 | 1 20.850
## 20 3_1 3 | 1 | 0 | 0 24.000
## 21 3_2 3 | 2 | 0 | 0 25.000
## 22 3_3 2 | 3 | 0 | 0 25.000
## 23 3_4 3 | 2 | 0 | 0 25.000
## 24 3_5 3 | 2 | 0 | 0 25.000
## 25 4_0 4 | 0 | 0 | 0 24.000
## 26 4_1 4 | 1 | 0 | 0 25.000
## 27 4_2 3 | 2 | 0 | 0 25.000
## 28 4_3 2 | 3 | 0 | 0 25.000
## 29 4_4 2 | 3 | 0 | 0 25.000
## 30 4_5 2 | 3 | 0 | 0 25.000
## 31 5_0 5 | 0 | 0 | 0 25.000
## 32 5_1 4 | 1 | 0 | 0 25.000
## 33 5_2 3 | 2 | 0 | 0 25.000
## 34 5_3 2 | 3 | 0 | 0 25.000
## 35 5_4 2 | 3 | 0 | 0 25.000
## 36 5_5 2 | 3 | 0 | 0 25.000

If we specify static actions via static.A.fun, orstatic.A1 and static.A2 it means that each period consists of two stages in which actions can be chosen (each preceded by a stage in which transfers can be performed). First, the static actions are chosen and then the dynamic actions. Splitting up actions in this fashion can substantially reduce numerical complexity in form of memory requirements and run time of the solution algorithms.

Note that we have also have specified here the parameters x.max, i.cost and a via the function rel_param. All the specified parameters will be passed to the different functions like trans.fun that specify the game and can be used in expressions like pi1 that will be lazily evaluated.

11. Make players move in turns

There are some advantages to specify the game such that each period only a randomly selected player is able to make investment. For example, with simultaneous investment choices sometimes no pure strategy equilibria may exist. See the 2nd vignette how to implement and analyse such games with staggered moves.

12. Specifying parameters inside the game

So far we have specified the parameters x.max, i.cost and a as global variables. Alternatively, you could also specify them in the game using the function rel_param. Consider the following variant:

# Action space
A.fun = function(x1,x2,x.max,...) {
  list(
    A1=list(q1 = 0:x1, i1=c(0, if (x1 < x.max) 1)),
    A2=list(q2 = 0:x2, i2=c(0, if (x2 < x.max) 1))
  )
}

# State transitions
trans.fun = function(ax.df,x.max,...) {
  restore.point("trans.fun")
  ax.df %>%
    select(x,x1,x2,i1,i2) %>%
    unique() %>%
    mutate(
      new_x1 = pmin(x1+i1,x.max),
      new_x2 = pmin(x2+i2,x.max),
      xd = paste0(new_x1,"_",new_x2),
      xs=x,
      prob = 1
    )
}

# State matrix
x.max = 5
x.df = tidyr::expand_grid(x1=0:x.max, x2=0:x.max) %>%
  mutate(x=paste0(x1,"_",x2))

g = rel_game("Cournot with Investment") %>%
  rel_param(a = 10,i.cost = 1.5,x.max=x.max) %>%
  rel_states(x=x.df,
    A.fun=A.fun,
    pi1 = (a-(q1+q2))*q1 - i.cost*i1,
    pi2 = (a-(q1+q2))*q2 - i.cost*i2,
    trans.fun=trans.fun
  )

g %>% rel_spe(delta=0.3) %>% get_eq() %>% select(x, ae.lab,U)
## # A tibble: 36 x 3
##    x     ae.lab        U
##    <chr> <chr>     <dbl>
##  1 0_0   0 1 | 0 1  2.84
##  2 0_1   0 1 | 1 1 10.5 
##  3 0_2   0 1 | 2 0 16.4 
##  4 0_3   0 1 | 3 0 20.8 
##  5 0_4   0 0 | 4 0 24.0 
##  6 0_5   0 0 | 5 0 25   
##  7 1_0   1 1 | 0 1 10.5 
##  8 1_1   1 0 | 1 1 16.4 
##  9 1_2   1 0 | 2 0 21   
## 10 1_3   1 0 | 3 0 24.0 
## # ... with 26 more rows

The specified parameters will then be passed as additional arguments to A.fun, trans.fun etc and will also be used when pi1 and pi2 are evaluated.

If you want to create at the same time several game obects with different parameters, it seems advisable to store parameters inside the game and not rely on global variables. Otherwise it is a matter of taste, which style you want to use.