R/environment_gridworld.R

Defines functions getIntoBounds applyWind go.rightdown go.rightup go.leftdown go.leftup go.down go.up go.right go.left go makeRewardMatrix visualizeGridworld

#' Gridworld
#'
#' Creates gridworld environments.
#'
#' A gridworld is an episodic navigation task, the goal is to get from start state to goal state.
#'
#' Possible actions include going left, right, up or down. If `diagonal.moves = TRUE` diagonal
#' moves are also possible, leftup, leftdown, rightup and rightdown.
#'
#' When stepping into a cliff state you get a reward of `reward.cliff`,
#' usually a high negative reward and transition to a state specified by `cliff.transition.states`.
#'
#' In each column a deterministic wind specified via `wind` pushes you up a specific number of
#' grid cells (for the next action).
#'
#' A stochastic gridworld is a gridworld where with probability `stochasticity` the next state
#' is chosen at random from all neighbor states independent of the actual action.
#'
#' If an action would take you off the grid, the new state is the nearest cell inside the grid.
#' For each step you get a reward of `reward.step`, until you reach a goal state,
#' then the episode is done.
#'
#' States are enumerated row-wise and numeration starts with 0.
#' Here is an example 4x4 grid:
#' \tabular{rrrr}{
#'  0 \tab 1 \tab 2 \tab 3 \cr
#'  4 \tab 5 \tab 6 \tab 7 \cr
#'  8 \tab 9 \tab 10 \tab 11 \cr
#'  12 \tab 13 \tab 14 \tab 15 \cr
#' }
#' So a board position could look like this (G: goal state, x: current state, C: cliff state):
#' \tabular{rrrr}{
#'  G \tab o \tab o \tab o \cr
#'  o \tab o \tab o \tab o \cr
#'  o \tab x \tab o \tab o \cr
#'  o \tab o \tab o \tab C \cr
#' }
#'
#' @section Usage:
#' `makeEnvironment("gridworld", shape = NULL, goal.states = NULL, cliff.states = NULL,
#'   reward.step = -1, reward.cliff = -100, diagonal.moves = FALSE, wind = rep(0, shape[2]),
#'   cliff.transition.states = NULL, cliff.transition.done = FALSE, stochasticity = 0, ...)`
#'
#' @param shape \[`integer(2)`] \cr
#'   Shape of the gridworld (number of rows x number of columns).
#' @param goal.states \[`integer`] \cr
#'   Goal states in the gridworld.
#' @param cliff.states \[`integer`] \cr
#'   Cliff states in the gridworld.
#' @param reward.step \[`integer(1)`] \cr
#'   Reward for taking a step.
#' @param cliff.transition.states \[`integer`] \cr
#'   States to which the environment transitions if stepping into the cliff.
#'   If it is a vector, all states will have equal probability.
#'   Only used when `cliff.transition.done == FALSE`,
#'   else specify the `initial.state` argument.
#' @param reward.cliff \[`integer(1)`] \cr
#'   Reward for taking a step in the cliff state.
#' @param diagonal.moves \[`logical(1)`] \cr
#'   Should diagonal moves be allowed?
#' @param wind \[`integer`] \cr
#'   Strength of the upward wind in each cell.
#' @param cliff.transition.done \[`logical(1)`] \cr
#'   Should the episode end after stepping into the cliff?
#' @param stochasticity \[`numeric(1)`] \cr
#'   Probability of random transition to any of the neighboring states when taking any action.
#' @param ... \[`any`] \cr Arguments passed on to [makeEnvironment].
#'
#' @name Gridworld
#' @md
#' @inheritSection Environment Methods
#' @export
#'
#' @examples
#' # Gridworld Environment (Sutton & Barto Example 4.1)
#' env1 = makeEnvironment("gridworld", shape = c(4L, 4L), goal.states = 0L,
#'   initial.state = 15L)
#' env1$reset()
#' env1$visualize()
#' env1$step(0L)
#' env1$visualize()
#'
#' # Windy Gridworld (Sutton & Barto Example 6.5)
#' env2 = makeEnvironment("gridworld", shape = c(7, 10), goal.states = 37L,
#'   reward.step = -1, wind = c(0, 0, 0, 1, 1, 1, 2, 2, 1, 0),
#'   initial.state = 30L)
#'
#' # Cliff Walking (Sutton & Barto Example 6.6)
#' env3 = makeEnvironment("gridworld", shape = c(4, 12), goal.states = 47L,
#'   cliff.states = 37:46, reward.step = -1, reward.cliff = -100,
#'   cliff.transition.states = 36L, initial.state = 36L)
NULL

Gridworld = R6::R6Class("Gridworld",
  inherit = MdpEnvironment,

  public = list(

    initialize = function(shape = NULL, goal.states = NULL, cliff.states = NULL,
      reward.step = -1, reward.cliff = -100, diagonal.moves = FALSE, wind = rep(0, shape[2]),
      cliff.transition.states = NULL, cliff.transition.done = FALSE, stochasticity = 0, ...) {

      checkmate::assertIntegerish(shape, len = 2)
      if (prod(shape) <= 1) {
        stop("A gridworld with only one state is not allowed!")
      }
      checkmate::assertIntegerish(goal.states)
      goal.states = goal.states + 1
      checkmate::assertIntegerish(cliff.states, null.ok = TRUE)
      if (!is.null(cliff.states)) {
        cliff.states = cliff.states + 1
      }
      checkmate::assertIntegerish(cliff.transition.states, null.ok = TRUE)
      if (!is.null(cliff.transition.states)) {
        cliff.transition.states = cliff.transition.states + 1
      }
      if (any(goal.states > prod(shape)) || any(cliff.states > prod(shape)) |
          any(cliff.transition.states > prod(shape))) {
        stop("All states must be inside the grid! States are numerated row-wise starting with 0, check Details!")
      }
      checkmate::assertIntegerish(wind, len = shape[2])
      checkmate::assertNumber(reward.step)
      checkmate::assertNumber(reward.cliff)
      checkmate::assertFlag(diagonal.moves)
      checkmate::assertFlag(cliff.transition.done)
      checkmate::assertNumber(stochasticity, lower = 0, upper = 1)

      n.states = prod(shape)
      states = seq_len(n.states)
      n.col = shape[2]
      if (diagonal.moves) {
        n.actions = 8
        action.names = c("left" = 0L, "right" = 1L, "up" = 2L, "down" = 3L,
          "leftup" = 4L, "leftdown" = 5L, "rightup" = 6L, "rightdown" = 7L)
      } else {
        n.actions = 4
        action.names = c("left" = 0L, "right" = 1L, "up" = 2L, "down" = 3L)
      }

      rewards = makeRewardMatrix(reward.step, reward.cliff, n.states, n.actions,
        cliff.states, goal.states)

      transitions = array(matrix(0, nrow = n.states, ncol = n.states),
        dim = c(n.states, n.states, 8))

      border.states = list(left = seq(1, n.states - n.col + 1, n.col),
        right = seq(n.col, n.states, n.col),
        up = seq(1, n.col),
        down = seq(n.states - n.col + 1, n.states))

      non.terminal.states = setdiff(states, c(goal.states, cliff.states))
      actions = list("left", "right", "up", "down", "leftup",
        "leftdown", "rightup", "rightdown")
      actions = lapply(actions, function(x) {class(x) = x; x})

      m.cliff = NULL
      if (cliff.transition.done) {
        goal.states = c(goal.states, cliff.states)
      } else {
        if (!is.null(cliff.states)) {
          if (!is.null(cliff.transition.states)) {
            cliff.pairs = as.matrix(expand.grid(cliff.states, cliff.transition.states))
            cliff.prob = 1 / length(cliff.transition.states)
            m.cliff = cbind(cliff.pairs, cliff.prob)
          } else {
            non.terminal.states = c(non.terminal.states, cliff.states)
          }
        }
      }

      n.states = length(non.terminal.states)
      new.states = vapply(actions, go, states = non.terminal.states, border.states = border.states,
        n.col = n.col, FUN.VALUE = numeric(n.states))

      if (!is.matrix(new.states)) {
        new.states = as.matrix(new.states, nrow = 1)
      }

      m.stoch = matrix(0, nrow = n.states * 8, ncol = 3)
      m.stoch[, 1] = rep(non.terminal.states, 8)
      m.stoch[, 2] = c(new.states)
      m.stoch[, 3] = stochasticity / 8

      m.goal = matrix(c(goal.states, goal.states, rep(1, length(goal.states))), ncol = 3)
      m = rbind(m.cliff, m.goal, m.stoch)
      m = m[rep(seq_len(nrow(m)), each = 8), ]
      m = cbind(m, action = rep(1:8, nrow(m) / 8))

      new.states = c(apply(new.states, 2, applyWind, states = non.terminal.states, n.col = n.col, wind = wind))
      new.states = getIntoBounds(new.states, n.col = n.col)

      m2 = matrix(c(rep(non.terminal.states, 8), new.states, rep(1 - stochasticity, length(new.states)),
        rep(1:8, each = length(non.terminal.states))), ncol = 4)
      m = rbind(m, m2)
      colnames(m) = c("row", "col", "prob", "action")

      m = as.matrix(aggregate(prob ~ row + col + action, data = as.data.frame(m), FUN = "sum"))
      transitions[m[, c("row", "col", "action")]] = m[, "prob"]
      transitions = transitions[, , seq_len(n.actions)]

      visualize = function(env) {
        message(cat(visualizeGridworld(shape, env$state), "\n"))
      }
      # fixme: make action.names and visualize overwriteable
      super$initialize(transitions = transitions, rewards = rewards,
        visualize = visualize, action.names = action.names, ...)
    }
  )
)

visualizeGridworld = function(shape, current.state) {
  one.row = paste(rep("-", shape[2]), collapse = " ")
  grid.vis = paste("", one.row, collapse = "")
  for (i in seq_len(shape[1] - 1L)) {
    grid.vis = paste(grid.vis, "\n", one.row)
  }

  n = current.state + 1
  # find position of nth -
  str.pos = gregexpr("-", grid.vis)[[1]][n]
  # replace nth - with o (current state in grid)
  grid.vis = sub(paste0("^(.{", str.pos - 1, "})(.)(.*$)", collapse = ""),
    "\\1o\\3", grid.vis)
  grid.vis
}

makeRewardMatrix = function(reward.step, reward.cliff, n.states, n.actions,
  cliff.states, goal.states) {
  rewards = matrix(reward.step, nrow = n.states, ncol = n.actions)
  rewards[cliff.states, ] = reward.cliff
  rewards[goal.states, ] = 0
  rewards
}

go = function(x, ...) {
  UseMethod("go", x)
}

#' @export
go.left = function(x, states, border.states, ...) {
  ifelse(states %in% border.states[["left"]], states, states - 1)
}

#' @export
go.right = function(x, states, border.states, ...) {
  ifelse(states %in% border.states[["right"]], states, states + 1)
}

#' @export
go.up = function(x, states, border.states, n.col) {
  ifelse(states %in% border.states[["up"]], states, states - n.col)
}

#' @export
go.down = function(x, states, border.states, n.col) {
  ifelse(states %in% border.states[["down"]], states, states + n.col)
}

#' @export
go.leftup = function(x, states, border.states, n.col) {
  go.left(x, go.up(x, states, border.states, n.col), border.states)
}

#' @export
go.leftdown = function(x, states, border.states, n.col) {
  go.left(x, go.down(x, states, border.states, n.col), border.states)
}

#' @export
go.rightup = function(x, states, border.states, n.col) {
  go.right(x, go.up(x, states, border.states, n.col), border.states)
}

#' @export
go.rightdown = function(x, states, border.states, n.col) {
  go.right(x, go.down(x, states, border.states, n.col), border.states)
}

applyWind = function(states, new.states, wind, n.col) {
  column = states %% n.col
  column[column == 0] = n.col
  new.states - wind[column] * n.col
}

getIntoBounds = function(new.states, n.col) {
  while (any(new.states <= 0)) {
    new.states[new.states <= 0] = new.states[new.states <= 0] + n.col
  }
  new.states
}

#' Cliff Walking
#'
#' Gridworld environment for reinforcement learning from Sutton & Barto (2017).
#' Grid of shape 4x12 with a goal state in the bottom right of the grid.
#' Episodes start in the lower left state. Possible actions include going left, right, up and down.
#' Some states in the lower part of the grid are a cliff,
#' so taking a step into this cliff will yield a high negative reward of - 100 and move the agent
#' back to the starting state.
#' Elsewise rewards are - 1, for the goal state 0.
#'
#' This is the gridworld (goal state denoted G, cliff states denoted C, start state denoted S):
#' \tabular{rrrrrrrrrrrr}{
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  S \tab C \tab C \tab C \tab C \tab C \tab C \tab C \tab C \tab C \tab C \tab G \cr
#' }
#'
#' @section Usage:
#' `makeEnvironment("cliff.walking", ...)`
#'
#' @param ... \[`any`] \cr Arguments passed on to [makeEnvironment].
#'
#' @md
#'
#' @name CliffWalking
#' @aliases cliff.walking
#'
#' @references Sutton and Barto (Book draft 2017): Reinforcement Learning: An Introduction Example 6.6
#' @export
#'
#' @inheritSection Environment Methods
#' @examples
#' env = makeEnvironment("cliff.walking")
NULL

CliffWalking = R6::R6Class("CliffWalking",
  inherit = Gridworld,
  public = list(
    initialize = function(...) {
      super$initialize(shape = c(4, 12), goal.states = 47,
        cliff.states = 37:46, reward.step = -1, reward.cliff = -100,
        cliff.transition.states = 36, initial.state = 36, ...)
    }
  )
)

#' Windy Gridworld
#'
#' Windy Gridworld problem for reinforcement learning. Actions include
#' going left, right, up and down. In each column the wind pushes you up a
#' specific number of steps (for the next action). If an action would
#' take you off the grid, you remain in the previous state. For each step you
#' get a reward of -1, until you reach into a terminal state.
#'
#' This is the gridworld (goal state denoted G, start state denoted S).
#' The last row specifies the upward wind in each column.
#' \tabular{rrrrrrrrrr}{
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  S \tab . \tab . \tab . \tab . \tab . \tab . \tab G \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \tab . \cr
#'  0 \tab 0 \tab 0 \tab 1 \tab 1 \tab 1 \tab 2 \tab 2 \tab 1 \tab 0 \cr
#' }
#'
#' @section Usage:
#' `makeEnvironment("windy.gridworld", ...)`
#'
#' @param ... \[`any`] \cr Arguments passed on to [makeEnvironment].
#'
#' @md
#'
#' @name WindyGridworld
#' @aliases windy.gridworld
#'
#' @references Sutton and Barto (Book draft 2017): Reinforcement Learning: An Introduction Example 6.5
#' @export
#'
#' @inheritSection Environment Methods
#' @examples
#' env = makeEnvironment("windy.gridworld")
NULL

WindyGridworld = R6::R6Class("WindyGridworld",
  inherit = Gridworld,
  public = list(
    initialize = function(...) {
      super$initialize(shape = c(7, 10), goal.states = 37,
        reward.step = -1, wind = c(0, 0, 0, 1, 1, 1, 2, 2, 1, 0),
        initial.state = 30, ...)
    }
  )
)

# fixme: add character actions (e.g. "left")
markusdumke/reinforcelearn documentation built on Nov. 20, 2022, 8:09 p.m.