Skip to content

Commit 99c4e9e

Browse files
committed
update
Added ‘maze.r’ which uses Randomized Prim’s Algorithm to build a maze.
1 parent 682b0c0 commit 99c4e9e

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed

maze.r

+321
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
# Randomized Prim's Algorithm
2+
# Choose a random cell within the maze grid (given by its width and height) and design it as a start cell.
3+
# Add the start cell to (by now empty) inCells set.
4+
# Mark cells around the start cell as frontier, i.e. add them to frontierCells set.
5+
# While frontierCells set is not empty:
6+
# Choose a random frontier cell cF from frontierCells.
7+
# Choose a random in-cell cI adjacent to cF.
8+
# Add cF to inCells.
9+
# Mark all out-cells around cF as frontier.
10+
# Add a path between cI and cF to the maze paths set.
11+
# Remove cF from frontierCells set
12+
#################
13+
# Set slowly to any value to sleep that much per drawing cycle
14+
pmaze <- function(dimx, dimy, slowly=FALSE, tcols, ...) {
15+
if(missing(tcols) ) tcols <- rainbow((dimx+dimy)/10) # a wag at count
16+
#plotting the full grid:
17+
mardef<-par()$mar
18+
par(mar=c(.1,.1,.1,.1))
19+
#build init matrices; remember cells are ID'd by "top left" coord
20+
icell<-matrix(0,nrow=(dimy),ncol=(dimx) )
21+
fcell<-matrix(0,nrow=(dimy),ncol=(dimx) )
22+
walls <- NULL #store indices of removed walls
23+
dewall<-vector(length=4)
24+
# select starting cell
25+
initx<-sample(1:dimx,1)
26+
inity<-sample(1:dimy,1)
27+
icell[inity,initx] <- 1
28+
# calculate frontier cells from that cell. Watch for borders!
29+
# zero values are ignored, hence the mod func
30+
fcell[inity, c(initx-1,(initx+1)%%(dimx+1) )]<-1
31+
fcell[c(inity-1,(inity+1)%%(dimy+1) ), initx]<-1
32+
#OK, so now fcell is nonempty, and can start the while loop
33+
while (sum(fcell) > 0 ) {
34+
# pick a random frontier cell.
35+
fronts <- which (fcell>0, arr.ind=TRUE)
36+
fgrab <- fronts[sample(1:nrow(fronts),1),]
37+
doorx <- fgrab[2] + c(1,-1)
38+
doorx <- doorx[doorx %in% 1:dimx]
39+
doory <- fgrab[1] + c(1,-1)
40+
doory <- doory[doory %in% 1:dimy]
41+
# want only the "cross" 4 cells, and only the ones which are in Icell
42+
adjac<-NULL
43+
for (j in 1:length(doorx) ) if( icell[fgrab[1],doorx[j]]>0 ) adjac<-rbind(adjac,c(fgrab[1],doorx[j]) )
44+
for (j in 1:length(doory) ) if( icell[doory[j],fgrab[2]]>0 ) adjac<-rbind(adjac,c(doory[j],fgrab[2]) )
45+
# pick one at random
46+
igrab <- adjac[sample(1:nrow(adjac),1),]
47+
# Now figure out the coordinates of the endpoints of this segment.
48+
#go from (max_row_ind,max_col_ind) and increment whichever index
49+
# was the same for both.
50+
dewall[2] <- max(igrab[1],fgrab[1])
51+
dewall[1] <- max(igrab[2],fgrab[2])
52+
dewall[4] <- dewall[2] + 1*(igrab[1]==fgrab[1])
53+
dewall[3] <- dewall[1] + 1*(igrab[2]==fgrab[2])
54+
# add it to a matrix.
55+
walls <- rbind(walls,dewall)
56+
#clean up
57+
rownames(walls) <- NULL
58+
colnames(walls) <- c('x1', 'y1','x2','y2')
59+
# need to zero the frontier cell and unzero the matching icell
60+
fcell[fgrab[1],fgrab[2]] <-0
61+
icell[fgrab[1],fgrab[2]] <-1
62+
# and set neighbors of fgrab to 1 in fcell.
63+
# But there's more: do NOT assign a "former" frontier cell even if it's a
64+
# neighbor of the current fgrab. This can be done by skipping neighbors
65+
# whose icell value is 1.
66+
for (j in 1:length(doorx) )
67+
if( icell[fgrab[1],doorx[j]] == 0 ) fcell[fgrab[1], doorx[j]] <- 1
68+
for (j in 1:length(doory) )
69+
if( icell[doory[j],fgrab[2]] == 0 ) fcell[doory[j], fgrab[2]] <- 1
70+
# now rinse lather repeat.
71+
} # end of while
72+
# modify limits to make it easier to plot in/out labels
73+
plot( c(1,(dimx+1),(dimx+1),1,1), c(1,1,(dimy+1),dimy+1,1),t='l',axes=F,xlab='',ylab='',xlim=c(0,dimx+2),ylim=c(0,dimy+2) )
74+
df = as.matrix(expand.grid( xvert= seq_len(dimx),yvert= seq_len(dimy)))
75+
dfv <- cbind(df,df[,1],df[,2]+1)
76+
df2 <- as.matrix(expand.grid(yvert= seq_len(dimy), xvert= seq_len(dimx)))
77+
dfh <- cbind(df2[,2],df2[,1],df2[,2]+1,df2[,1])
78+
# concatenate each row w/ delimiter (so that 2_13 is not same as 21_3 )
79+
#nb alternative methods as found on SO turn out to be much slower
80+
allwalls<-rbind(dfv,dfh)
81+
allrows<-unlist(sapply(1:nrow(allwalls),function(j) paste(allwalls[j,],collapse='_')) )
82+
# the maze walls:
83+
allfoo <- unlist(sapply(1:nrow(walls),function(j) paste(walls[j,],collapse='_')))
84+
thewalls<-setdiff(allrows,allfoo)
85+
dowalls<-allwalls[allrows%in%thewalls,]
86+
###################################################################
87+
# New project: start at any wall touching border, and plot all segments in
88+
# that 'tree,' defineds as segments w/ common endpoints. Then pick a remaining
89+
# segment touching border, etc.
90+
# Note: fortunately, which(dist(rbind(foo,empty_matrix))) returns nothing rather
91+
# than any crash.
92+
# pick any wall to start, remove it from starting set
93+
treecnt<-1
94+
#kill edge walls -oops , not equality but equal to 1 or nrow/ncol
95+
vedges <- which( (dowalls[,1]==dowalls[,3]) & (dowalls[,1]==1 | dowalls[,1]==dimx+1) )
96+
hedges <- which( (dowalls[,2]==dowalls[,4]) & (dowalls[,2]==1 | dowalls[,2]==dimy+1) )
97+
dowalls<-dowalls[-c(vedges,hedges),,drop=FALSE]
98+
# now sort into trees
99+
# this appears to take rather a while. What can I vectorize?
100+
while(nrow(dowalls)>0 ) {
101+
tree <- matrix(dowalls[1,],nr=1) #force dimensions
102+
dowalls<-dowalls[-1,,drop=FALSE]
103+
treerow <- 1 #current row of tree we're looking at
104+
while ( treerow <= nrow(tree) ) {
105+
#only examine the first 'column' of the dist() matrix 'cause those are the
106+
# distances from the tree[] endpoints
107+
# Still seems to me that one bigass dist(dowalls) matrix should be
108+
# sortable by identified row/column per tree[] element, thus saving
109+
# an absurd amount of processing. Let's get on that
110+
touch <- c( which(dist(rbind(tree[treerow,1:2],dowalls[,1:2]) )[1:nrow(dowalls)]==0), which(dist(rbind(tree[treerow,1:2],dowalls[,3:4]) )[1:nrow(dowalls)]==0), which(dist(rbind(tree[treerow,3:4],dowalls[,1:2]) )[1:nrow(dowalls)]==0), which(dist(rbind(tree[treerow,3:4],dowalls[,3:4]) )[1:nrow(dowalls)]==0) )
111+
if(length(touch) ) {
112+
tree <- rbind(tree,dowalls[c(touch),])
113+
dowalls <- dowalls[-c(touch),,drop=FALSE]
114+
}
115+
# track the row of tree[] we're working with AND track how many rows there currently are in tree[]
116+
treerow <- treerow + 1
117+
} #end of while treerow <= nrow
118+
for (k in 1:nrow(tree) ) {
119+
lines(tree[k,c(1,3)],tree[k,c(2,4)] , col=tcols[treecnt%%(length(tcols)-1) +1 ] )
120+
if(slowly) Sys.sleep(slowly)
121+
}
122+
treecnt <- treecnt + 1
123+
} #end of while dwalls exists
124+
# end of new project
125+
# original method:
126+
# dowalls <- dowalls[order(rowSums(dowalls[,1:2])),]
127+
# timng is kinda cruddy here. Experiment with (if(slowly & !j%%10)) sort of thing
128+
# for(j in 1:nrow(dowalls) ) {
129+
# lines(dowalls[j,c(1,3)],dowalls[j,c(2,4)],...)
130+
# if(slowly) Sys.sleep(slowly)
131+
# }
132+
##
133+
par(mar=c(mardef))
134+
return(invisible(walls))
135+
}
136+
#
137+
#################################
138+
#
139+
# This will "kick off" my "fill dead end" function,
140+
fillij <- function(wallmat, into=c(1,1), outof=c(apply(wallmat[,1:2],2, max)),...) {
141+
dimx<-max(wallmat[,1])
142+
dimy<-max(wallmat[,2])
143+
ijfoo<-cbind(expand.grid(x=1:dimx,y=1:dimy), matrix(0, nc=4))
144+
for(j in 1:(nrow(wallmat)) ) {
145+
xind <- wallmat[j,1]
146+
yind <- wallmat[j,2]
147+
# Another win for DWIM!
148+
ijrow <- which(ijfoo[,1]==xind & ijfoo[,2]==yind)
149+
if( xind == wallmat[j,3]) {
150+
# OK, verified that initial ijfoo is dead on (at least for test case)
151+
ijfoo[ijrow,6]<- 1
152+
# we never remove a vertical wall where x==1, so ijrow-1 is OK
153+
ijfoo[(ijrow-1),4] <- 1
154+
} else {
155+
#only other possibility is horiz wall
156+
ijfoo[ijrow,3] <- 1
157+
# want the row with same x values
158+
# and y values one less: ijrow- dimx , (dimx is the fast-changing index)
159+
ijfoo[ijrow-dimx,5] <- 1
160+
}
161+
} #end for (j) loop
162+
# now turn any deadend into an x,y,0,0,0,0 row and assign a zero to
163+
# the matching wall of its neighbor. But first...
164+
# lockdown 'into' and 'outof' rows by setting to 5 and 6
165+
oldijfoo<-ijfoo
166+
inrow <- into[1]+dimx*(into[2]-1)
167+
outrow <- outof[1] + dimx*(outof[2]-1)
168+
ijfoo[inrow,3:6]<-5
169+
ijfoo[outrow,3:6]<-6
170+
startijfoo<-ijfoo
171+
changed <- TRUE
172+
while (changed) {
173+
deads <- which(rowSums(ijfoo[,3:6]) == 1 )
174+
# aha: if have two deads, and the first one cleans out the second one,
175+
# that's where I get the zero-length fillwall!
176+
for (k in deads) {
177+
#find the live wall and so on. k= 1,2,3,4 --> N,E,S,W
178+
fillwall<-which(ijfoo[k,3:6] ==1 )
179+
if( length(fillwall) > 0) {
180+
ijfoo[k,(fillwall+2)] <- 0
181+
# and its neighbor...
182+
switch(fillwall,
183+
{ijfoo[(k - dimx),5] <- 0 },
184+
{ijfoo[(k + 1),6] <- 0 },
185+
{ijfoo[(k + dimx),3] <- 0 },
186+
{ijfoo[ (k - 1),4] <- 0} )
187+
}
188+
} #end for k in deads loop
189+
# reset start and end cells to 'lockout' value, just in case
190+
ijfoo[inrow,3:6]<-5
191+
ijfoo[outrow,3:6]<-6
192+
changed <- !identical(oldijfoo,ijfoo)
193+
oldijfoo<-ijfoo
194+
}
195+
# so now ijfoo has been reduced to valid paths; all other rows sum to zero
196+
# label start and stop
197+
incell<-which(ijfoo[,3] == 5)
198+
outcell<-which(ijfoo[,3] ==6)
199+
xfoo<-ijfoo[incell,1]
200+
yfoo<-ijfoo[incell,2]
201+
text(xfoo-1.5*(xfoo==1)+ .5+ 1.5*(xfoo>=dimx), yfoo-(yfoo==1)+(yfoo>=dimy) + .5,'IN',font=2,cex=.6)
202+
xfoo<-ijfoo[outcell,1]
203+
yfoo<-ijfoo[outcell,2]
204+
text(xfoo-1.5*(xfoo==1)+ .5+ 1.5*(xfoo>=dimx), yfoo-(yfoo==1)+(yfoo>=dimy) + .5, 'OUT',font=2,cex=.6)
205+
return(invisible(ijfoo))
206+
}
207+
208+
############################ draw the path
209+
#
210+
# wallmat: matrix generated with pmaze() containing "door" vertex coordinates
211+
# into: coordinates of cell for start/entering the maze
212+
# outof: coordinates of cell at finish of maze
213+
# color: color of path drawn
214+
# sleeptime: delay between increments of path drawing
215+
# ... : optional arguments compatible with lines()
216+
dopath<-function(wallmat, into=c(1,1), outof=c(apply(wallmat[,1:2],2, max)), color='red', sleeptime=0, ...) {
217+
dimx<-max(wallmat[,1])
218+
dimy<-max(wallmat[,2])
219+
# do input validation. But if someone sends a negative dim,poop on him
220+
into <- c(min(into[1],dimx), min(into[2],dimy) )
221+
outof <- c(min(outof[1],dimx), min(outof[2],dimy) )
222+
# call fillij() here
223+
pathmat<-fillij(wallmat, into,outof )
224+
# remove deadends - I think this is redundant, but leave in for the moment
225+
pathmat <- pathmat[rowSums(pathmat[,3:6])>0,]
226+
# Start at IN, find the location of distance==1, that may be in
227+
# either the IN row or IN column of distmat. Draw the path, delete/change
228+
# that "1" value and find the distance==1 for the adjacent cell we just found,
229+
# and so on until we find the "END" cell.
230+
startcell<- which(pathmat[,3]==5)
231+
distmat<- dist(pathmat[,1:2]) # object vector class 'dist'
232+
matdist<-as.matrix(distmat)
233+
mbar<-lower.tri(matdist)*matdist #kill upper triangle
234+
# run until startcell has "6" in it
235+
while (pathmat[startcell,3] != 6 ) {
236+
nextcell<- c(which(mbar[,startcell]==1),which(mbar[startcell,]==1))
237+
# fix to "bad" distance==1 locations (with overkill):
238+
mbar[nextcell,startcell] <- 0
239+
mbar[startcell,nextcell] <- 0
240+
## that works because I will never want to "find" startcell as a
241+
## "nextcell" to some other cell.
242+
if(length(nextcell)>1) {
243+
mated<-vector(length=length(nextcell))
244+
startloc<-pathmat[startcell,1:2] #move outside jj loop duh
245+
for( jj in 1: length(nextcell)) {
246+
## compare Nvs S and E vs W . Want a pair to be "1" --necessary but not sufficient
247+
delcell <- ( pathmat[startcell,3:6] * pathmat[nextcell[jj],c(5,6,3,4)] )
248+
## now reject NS or EW if it's in wrong direction, i.e. calculate which wall/door
249+
## the two have in common. if startcell is N of nextcell, y1<y2 . and so on.
250+
nextloc <- pathmat[nextcell[jj],1:2]
251+
matchwall<-c(startloc[2]>nextloc[2], startloc[1]<nextloc[1], startloc[2]<nextloc[2], startloc[1]>nextloc[1])
252+
# mated is TRUE only when they share a door
253+
mated[jj]<- sum(delcell * matchwall )
254+
}
255+
## so now set actual nextcell to the mated==TRUE element
256+
nextcell<-nextcell[as.logical(mated)]
257+
} #end of if(length) block
258+
# loop over NESW
259+
for (j in 3:6) {
260+
# create the offsets for the given column
261+
offs <- c( 0.5*(-1)^(j==6), 0.5 + (j==4), 0.5*(-1)^(j==3), 0.5 +(j==5) )
262+
if ( pathmat[nextcell,j] == 1 ) {
263+
lines(pathmat[nextcell,1]+offs[1:2], pathmat[nextcell,2]+offs[3:4], col=color, ...)
264+
}
265+
}
266+
Sys.sleep(sleeptime)
267+
# update starting location
268+
startcell<-nextcell
269+
} # end of while(not in END cell)
270+
# get the path
271+
return(invisible(pathmat))
272+
}
273+
#
274+
############################ loop it
275+
## edited to handle any maze w/ compatible walls output
276+
# old "dopmaze" doesn't have a 'mazename' arg
277+
#dopmaze <- function(reps,slowly=.01, sleep=.2) {
278+
domaze <- function(reps,mazetype=c('rbmaze','pmaze'),slowly=.01, sleep=.2) {
279+
cols <- c('red','blue','green','purple','pink')
280+
dimrange <- 15:50
281+
for (j in 1: reps) {
282+
# change to a do.call
283+
# foo <- pmaze(sample(dimrange,1),sample(dimrange,1),slowly=slowly )
284+
foo<-do.call(mazetype, args=list(dimx=sample(dimrange,1),dimy=sample(dimrange,1),slowly=slowly) )
285+
# into and outof are forced to be on
286+
# the perimeter.
287+
dimx <- max(foo[,1])
288+
dimy <- max(foo[,2])
289+
insamp <- rbind(c(1,dimx,sample(2:(dimx-1),2)),c(sample(2:(dimy-1),2),1,dimy) )
290+
outsamp <- rbind(c(1,dimx,sample(2:(dimx-1),2)),c(sample(2:(dimy-1),2),1,dimy) )
291+
into <- c(insamp[,sample(1:4,1)] )
292+
outof <- c(outsamp[,sample(1:4,1)] )
293+
# semiBUG: don't allow IN and OUT to be too close to each other, or
294+
# the fact that dopath never zeroes out those rows will lead to a crash
295+
# force separation of IN and OUT
296+
while (dist(rbind(into,outof))< 4 ) {
297+
outsamp <- rbind(c(1,dimx,sample(2:(dimx-1),2)),c(sample(2:(dimy-1),2),1,dimy) )
298+
outof <- c(outsamp[,sample(1:4,1)] )
299+
}
300+
dopath(foo, into=into, outof=outof, col=sample(cols,1), sleep = sleep)
301+
Sys.sleep(5)
302+
}
303+
# just in case I'm interested.
304+
outs <- list(fmaze=foo, into=into, outof=outof)
305+
return(invisible(outs))
306+
}
307+
308+
######################### a simple "replot" function to take the
309+
# output of pmaze and redraw the maze
310+
repmaze<-function(walls) {
311+
mardef<-par()$mar
312+
dimx<-max(walls[,1])
313+
dimy<-max(walls[,2])
314+
par(mar=c(.1,.1,.1,.1))
315+
plot( c(1,(dimx+1)), c(1,(dimy+1)),t='n',axes=F,xlab='',ylab='')
316+
# kill the sapplys if go to post-wall cell plotting
317+
sapply(1:(dimy+1), function(j) lines(c(1,dimx+1),c(j,j)) )->ssfoo
318+
sapply(1:(dimx+1), function(j) lines(c(j,j),c(1,dimy+1)) )->ssfoo
319+
for( j in 1:nrow(walls) ) lines(walls[j,c(1,3)],walls[j,c(2,4)],col='white')
320+
par(mar=c(mardef))
321+
}

0 commit comments

Comments
 (0)