Recursive maze solver in R
Asked Answered
R

2

5

I want to solve a maze in R. I created a function inspired by corresponding Python code: Solving mazes using Python: Simple recursivity and A* search.

I have a maze (i.e. a matrix), where: 0 = empty space, 1 = wall (unreachable cell), 2 = finish, 3 = already visited.

Set up the maze:

data = c(rep(1, 20),
         c(4,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,1),
         c(1,1,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,2),
         rep(1, 20))

maze = matrix(data, 4, 20, byrow = TRUE)

My attempt:

search = function(x, y){
  if (maze[x,y] == 2){
    print(paste('i am in point', x, y))
    return(TRUE)
  } else if (maze[x,y]==1){
    print(paste('wall in point', x, y))
    return(FALSE)
  } else if (maze[x,y]==3){
    print(paste('visited point', x, y))
    return(FALSE)
  } 
    
  #set as marked
  print(paste('visited point', x, y))
  maze[x,y] = 3
    
  if((x < length(maze[,1])   & search(x+1, y))
       | (y > 1 & search(x,y-1))
       | (x > 1 & search(x-1,y))
       | (y < length(maze[1,]) & search(x,y+1))){
      return(TRUE)
  }
  
  return(FALSE)
}

search(x= 2, y = 1)

Unfortunately, the code errors:

[1] "visited point 2 1"  
[1] "wall in point 3 1" 
   
Error in if (maze[x, y] == 2) { : argument is of length zero

I see problem with else statement, because function stops on a cell which is empty, i.e. 0.

Recall answered 8/1, 2023 at 14:55 Comment(0)
A
4

I took a slightly different route than @Ben.

I wanted to see the routes, so I've also got two additional functions. The first one is colorPrint, which adds color to the '3's in the matrix. The second is directedPrint, which replaces the 3's with the arrows pointing to the direction of travel.

Most of the code in both functions is to rebuild the 'appearance' of a matrix. (cat would print a matrix a single string of numbers)

These functions are called in search every time '2' is found.

library(crayon)
library(tidyverse)

colorPrint <- function(mazish) {
  cols <- ncol(mazish)
  rows <- nrow(mazish)
  mazish <- gsub(", ", "    ",  # add color to values == 3
                 lapply(1:rows, 
                        function(k) {
                          toString(mazish[k, ])
                        }) %>% unlist()
  ) %>% gsub(pattern = "3", replacement = green("3"))
  rowing <- paste0("[", 1:rows, ",]") # rebuild matrix 'appearance' for cat
  coling <- paste0("[,", 1:cols, "]")
  # 4 spaces between each value 
  cat("     ", coling, "\n", paste0(rowing, "    ", mazish, "\n"))
}
directedPrint <- function(mazish, directed) {
  directed <- data.frame(matrix(unlist(directed), ncol = 2, 
                                nrow = length(directed), byrow = T)) %>% 
    setNames(c("x", "y")) %>% 
    mutate(direct = case_when( # determine the direction of travel
      lag(x) < x | lead(x) > x ~ '⬇', # first entry doesn't have any lags
      lag(y) < y | lead(y) > y ~ '⮕',
      lag(x) > x | lead(x) < x ~ '⬆',
      lag(y) > y | lead(y) < y ~ '⬅',
      TRUE ~ '·'))
  cols <- ncol(mazish)
  rows <- nrow(mazish)
  mazishD <- matrix(as.character(mazish), nrow = rows, ncol = cols) # char matrix
  lapply(1:nrow(directed),     # replace 3's with arrows
         function(j) {
           where <- 
             what <- directed[j, "direct"]
           mazishD[directed[j, "x"], directed[j, "y"]] <<- red(what)
         })
  rowing <- paste0("[", 1:rows, ",]") # build the 'appearance' of a matrix
  coling <- paste0("[,", 1:cols, "]")
  lmaz <- gsub(", ", "    ",          # prep for `cat` to appear as a matrix
               lapply(1:rows, 
                      function(k) {
                        toString(mazishD[k, ]) # 1 string per row for cat
                      }) %>% unlist()) %>% # arrows take more than 1 char space
    str_replace_all("    \033", paste0("   \U200A\U2006", "\033")) 
  rowSp <- ifelse(substr(lmaz, 1, 1) == "\033", "   \U200A\U2006", "    ")
  cat("     ", coling, "\n", paste0(rowing, rowSp, lmaz, "\n"))
}

Here's my search function.

search = function(maze, x, y, directions = NULL){
  if(!exists("directions")) {
    directions <- list()
  }
  directions <- append(directions, c(x, y))
  if (x == 0 | y == 0) {       # outside of matrix limits
    return(FALSE)
  } else if (x > nrow(maze) | y > ncol(maze)) { # outside of matrix limits
    return(FALSE)
  } else if (maze[x, y] == 2){
    print(paste('I am in point', x, y))
    colorPrint(maze)                    # print with 3's colored
    directedPrint(maze, directions)     # print solution with arrows
    return(TRUE)
  } else if (maze[x, y] == 1){
    print(paste('wall in point', x, y))
    return(FALSE)
  } else if (maze[x, y] == 3){
    print(paste('visited point', x, y))
    return(FALSE)
  }  
  
  #set as marked
  print(paste('visited point', x, y))
  maze[x, y] <- 3                       # annotate path of travel
  
  if((x < nrow(maze) & search(maze, x + 1, y, directions)) 
     | (y > 1 & search(maze, x, y - 1, directions))
     | (x > 1 & search(maze, x - 1, y, directions))
     | (y < ncol(maze) & search(maze, x, y + 1, directions))) {
    return(TRUE)
  }
  return(FALSE)
}

You'll use this similar to how you had initially described. However, I have to say that the data you provided has walls for the first 20 entries, meaning that there is nowhere for the maze search to go.

set.seed(253)
dt <- sample(c(
  sample(c(rep(0, 10), c(rep(1, 4))), 49, replace = T), # random 0s, 1s
  2), 50, replace = F) # only [1] 2

maze = matrix(dt, 5, 10, byrow = TRUE)

search(maze, 1, 1)

In the console, you'll see every possible route. However, I've only shown one route to the finish. (I made this an image so you would see the color & spacing.)

enter image description here

You could always add collectors so you could see how many times it cycled and how many steps to each solution were needed.

This uses an external list. Here is the updated search function. there is one new line where if == 2.

search = function(maze, x, y, directions = NULL){
  if(!exists("directions")) {
    directions <- list()
  }
  directions <- append(directions, c(x, y))
  if (x == 0 | y == 0) {       # outside of matrix limits
    return(FALSE)
  } else if (x > nrow(maze) | y > ncol(maze)) { # outside of matrix limits
    return(FALSE)
  } else if (maze[x, y] == 2){
    print(paste('I am in point', x, y))
    colorPrint(maze)                    # print with 3's colored
    directedPrint(maze, directions)     # print solution with arrows
    d[[length(d) + 1]] <<- length(directions) # how many directions were taken?
    
    return(TRUE)
  } else if (maze[x, y] == 1){
    print(paste('wall in point', x, y))
    return(FALSE)
  } else if (maze[x, y] == 3){
    print(paste('visited point', x, y))
    return(FALSE)
  }  
  
  #set as marked
  print(paste('visited point', x, y))
  maze[x, y] <- 3                       # annotate path of travel
  
  if((x < nrow(maze) & search(maze, x + 1, y, directions)) 
     | (y > 1 & search(maze, x, y - 1, directions))
     | (x > 1 & search(maze, x - 1, y, directions))
     | (y < ncol(maze) & search(maze, x, y + 1, directions))) {
    return(TRUE)
  }
  return(FALSE)
}

To use this one, you'll need to create a list in the global environment.

d = list()

dt2 <- matrix(data = c(rep(0, 5), 1, 
                       1, 1, 0, 0, 0, 1, 
                       0, 0, 0, 1, 0, 0, 
                       0, 1, 0, 0, 1, 0,
                       0, 1, 0, 0, 0, 2,
                       rep(0, 5), 1),
              nrow = 6, ncol = 6, byrow = F)
search(dt2, 1, 1)

This returned 43 solutions, where the shortest sequence was 20 steps & the longest was 48.

enter image description here

Anesthesiologist answered 13/1, 2023 at 20:4 Comment(0)
K
2

I think a few things may have been needed to make this work in R. First, python arrays are 0-indexed, but R is 1 indexed. So your error may be accessing a zero index element in the matrix. Also, you may need to feed back your maze matrix as an argument when recursively calling your function.

I adapted your demo based on the python example (6 x 6 matrix). The number '2' represents the finish position here. The function search will check if out of bounds separately. To see how the maze is solved, you can uncomment the print statement of maze.

data <- c(
  0, 0, 0, 0, 0, 1,
  1, 1, 0, 0, 0, 1,
  0, 0, 0, 1, 0, 0,
  0, 1, 1, 0, 0, 1,
  0, 1, 0, 0, 1, 0,
  0, 1, 0, 0, 0, 2
)

maze <- matrix(data, 6, 6, byrow = TRUE)

search <- function(maze, x, y) {
  
  # Check if out of bounds
  if (x < 1 | x > length(maze[,1])) return (F)
  if (y < 1 | y > length(maze[1,])) return (F)
  
  # Check if at end, already visited, or hitting a wall
  if (maze[x, y] == 2){
    print(paste('i am at the end: ', x, y))
    return(TRUE)
  } else if (maze[x, y] == 3){
    print(paste('already visited point: ', x, y))
    return(FALSE)
  } else if (maze[x, y] == 1){
    print(paste('wall in point: ', x, y))
    return(FALSE)
  } 
  
  # Set point as visited
  maze[x,y] = 3
  print(paste('visiting point: ', x, y))
  
  # Optional show maze as solved
  # print(maze)
  
  # Check clockwise positions for next move
  if (search(maze, x + 1, y)) return (T)
  if (search(maze, x, y - 1)) return (T)
  if (search(maze, x - 1, y)) return (T)
  if (search(maze, x, y + 1)) return (T)
  
  # No other move found
  return(F)
}

search(maze, x = 1, y = 1)

Output

[1] "visiting point:  1 1"
[1] "wall in point:  2 1"
[1] "visiting point:  1 2"
[1] "wall in point:  2 2"
[1] "already visited point:  1 1"
[1] "visiting point:  1 3"
[1] "visiting point:  2 3"
[1] "visiting point:  3 3"
[1] "wall in point:  4 3"
[1] "visiting point:  3 2"
[1] "wall in point:  4 2"
[1] "visiting point:  3 1"
[1] "visiting point:  4 1"
[1] "visiting point:  5 1"
[1] "visiting point:  6 1"
[1] "already visited point:  5 1"
[1] "wall in point:  6 2"
[1] "already visited point:  4 1"
[1] "wall in point:  5 2"
[1] "already visited point:  3 1"
[1] "wall in point:  4 2"
[1] "wall in point:  2 1"
[1] "already visited point:  3 2"
[1] "wall in point:  2 2"
[1] "already visited point:  3 3"
[1] "already visited point:  2 3"
[1] "wall in point:  3 4"
[1] "wall in point:  2 2"
[1] "already visited point:  1 3"
[1] "visiting point:  2 4"
[1] "wall in point:  3 4"
[1] "already visited point:  2 3"
[1] "visiting point:  1 4"
[1] "already visited point:  2 4"
[1] "already visited point:  1 3"
[1] "visiting point:  1 5"
[1] "visiting point:  2 5"
[1] "visiting point:  3 5"
[1] "visiting point:  4 5"
[1] "wall in point:  5 5"
[1] "visiting point:  4 4"
[1] "visiting point:  5 4"
[1] "visiting point:  6 4"
[1] "visiting point:  6 3"
[1] "wall in point:  6 2"
[1] "visiting point:  5 3"
[1] "already visited point:  6 3"
[1] "wall in point:  5 2"
[1] "wall in point:  4 3"
[1] "already visited point:  5 4"
[1] "already visited point:  6 4"
[1] "already visited point:  5 4"
[1] "visiting point:  6 5"
[1] "already visited point:  6 4"
[1] "wall in point:  5 5"
[1] "i am at the end:  6 6"
[1] TRUE

Maze Solution

     [,1] [,2] [,3] [,4] [,5] [,6]
[1,]    3    3    3    3    3    1
[2,]    1    1    3    3    3    1
[3,]    0    0    0    1    3    0
[4,]    0    1    1    3    3    1
[5,]    0    1    0    3    1    0
[6,]    0    1    0    3    3    2
Klayman answered 8/1, 2023 at 21:36 Comment(4)
it works , but i need to also return this new maze where 0 replace with 3, as a path for maze. In python it works automaticly, here i try to to place somewhere return(maze), but without results.Recall
@Recall To see the final maze, you can add print(maze) after print(paste('i am at then end: ', x, y)) - would that work for you? otherwise may require some restructuring...as the function is recursive, it is set up to return only TRUE or FALSE, not another object...Klayman
:( i need to have this updated maze after script run, because i would like to print the maze and chosen path. in python the maze was updated after every step ;/Recall
Another quick thing is to add maze <<- maze in the same place where made the finish --- this assigns maze in the global environment outside of the search function to the final maze with 3's filled in...would that work?Klayman

© 2022 - 2024 — McMap. All rights reserved.