|
| 1 | +package main |
| 2 | + |
| 3 | +// Code based on the Recursive backtracker algorithm. |
| 4 | +// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker |
| 5 | +// See https://youtu.be/HyK_Q5rrcr4 as an example |
| 6 | +// YouTube example ported to Go for the Pixel library. |
| 7 | + |
| 8 | +// Created by Stephen Chavez |
| 9 | + |
| 10 | +import ( |
| 11 | + "crypto/rand" |
| 12 | + "errors" |
| 13 | + "flag" |
| 14 | + "fmt" |
| 15 | + "math/big" |
| 16 | + "time" |
| 17 | + |
| 18 | + "github.com/faiface/pixel" |
| 19 | + "github.com/faiface/pixel/examples/community/maze/stack" |
| 20 | + "github.com/faiface/pixel/imdraw" |
| 21 | + "github.com/faiface/pixel/pixelgl" |
| 22 | + |
| 23 | + "github.com/pkg/profile" |
| 24 | + "golang.org/x/image/colornames" |
| 25 | +) |
| 26 | + |
| 27 | +var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35)) |
| 28 | +var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45)) |
| 29 | +var debug = false |
| 30 | + |
| 31 | +type cell struct { |
| 32 | + walls [4]bool // Wall order: top, right, bottom, left |
| 33 | + |
| 34 | + row int |
| 35 | + col int |
| 36 | + visited bool |
| 37 | +} |
| 38 | + |
| 39 | +func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) { |
| 40 | + drawCol := c.col * wallSize // x |
| 41 | + drawRow := c.row * wallSize // y |
| 42 | + |
| 43 | + imd.Color = colornames.White |
| 44 | + if c.walls[0] { |
| 45 | + // top line |
| 46 | + imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow))) |
| 47 | + imd.Line(3) |
| 48 | + } |
| 49 | + if c.walls[1] { |
| 50 | + // right Line |
| 51 | + imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize))) |
| 52 | + imd.Line(3) |
| 53 | + } |
| 54 | + if c.walls[2] { |
| 55 | + // bottom line |
| 56 | + imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize))) |
| 57 | + imd.Line(3) |
| 58 | + } |
| 59 | + if c.walls[3] { |
| 60 | + // left line |
| 61 | + imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow))) |
| 62 | + imd.Line(3) |
| 63 | + } |
| 64 | + imd.EndShape = imdraw.SharpEndShape |
| 65 | + |
| 66 | + if c.visited { |
| 67 | + imd.Color = visitedColor |
| 68 | + imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize))) |
| 69 | + imd.Rectangle(0) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) { |
| 74 | + neighbors := []*cell{} |
| 75 | + j := c.row |
| 76 | + i := c.col |
| 77 | + |
| 78 | + top, _ := getCellAt(i, j-1, cols, rows, grid) |
| 79 | + right, _ := getCellAt(i+1, j, cols, rows, grid) |
| 80 | + bottom, _ := getCellAt(i, j+1, cols, rows, grid) |
| 81 | + left, _ := getCellAt(i-1, j, cols, rows, grid) |
| 82 | + |
| 83 | + if top != nil && !top.visited { |
| 84 | + neighbors = append(neighbors, top) |
| 85 | + } |
| 86 | + if right != nil && !right.visited { |
| 87 | + neighbors = append(neighbors, right) |
| 88 | + } |
| 89 | + if bottom != nil && !bottom.visited { |
| 90 | + neighbors = append(neighbors, bottom) |
| 91 | + } |
| 92 | + if left != nil && !left.visited { |
| 93 | + neighbors = append(neighbors, left) |
| 94 | + } |
| 95 | + |
| 96 | + if len(neighbors) == 0 { |
| 97 | + return nil, errors.New("We checked all cells...") |
| 98 | + } |
| 99 | + return neighbors, nil |
| 100 | +} |
| 101 | + |
| 102 | +func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) { |
| 103 | + neighbors, err := c.GetNeighbors(grid, cols, rows) |
| 104 | + if neighbors == nil { |
| 105 | + return nil, err |
| 106 | + } |
| 107 | + nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors)))) |
| 108 | + if err != nil { |
| 109 | + panic(err) |
| 110 | + } |
| 111 | + randomIndex := nBig.Int64() |
| 112 | + return neighbors[randomIndex], nil |
| 113 | +} |
| 114 | + |
| 115 | +func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) { |
| 116 | + x := c.col * wallSize |
| 117 | + y := c.row * wallSize |
| 118 | + |
| 119 | + imd.Color = hightlightColor |
| 120 | + imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize))) |
| 121 | + imd.Rectangle(0) |
| 122 | +} |
| 123 | + |
| 124 | +func newCell(col int, row int) *cell { |
| 125 | + newCell := new(cell) |
| 126 | + newCell.row = row |
| 127 | + newCell.col = col |
| 128 | + |
| 129 | + for i := range newCell.walls { |
| 130 | + newCell.walls[i] = true |
| 131 | + } |
| 132 | + return newCell |
| 133 | +} |
| 134 | + |
| 135 | +// Creates the inital maze slice for use. |
| 136 | +func initGrid(cols, rows int) []*cell { |
| 137 | + grid := []*cell{} |
| 138 | + for j := 0; j < rows; j++ { |
| 139 | + for i := 0; i < cols; i++ { |
| 140 | + newCell := newCell(i, j) |
| 141 | + grid = append(grid, newCell) |
| 142 | + } |
| 143 | + } |
| 144 | + return grid |
| 145 | +} |
| 146 | + |
| 147 | +func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) { |
| 148 | + // Make an empty grid |
| 149 | + grid := initGrid(cols, rows) |
| 150 | + backTrackStack := stack.NewStack(len(grid)) |
| 151 | + currentCell := grid[0] |
| 152 | + |
| 153 | + return grid, backTrackStack, currentCell |
| 154 | +} |
| 155 | + |
| 156 | +func cellIndex(i, j, cols, rows int) int { |
| 157 | + if i < 0 || j < 0 || i > cols-1 || j > rows-1 { |
| 158 | + return -1 |
| 159 | + } |
| 160 | + return i + j*cols |
| 161 | +} |
| 162 | + |
| 163 | +func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) { |
| 164 | + possibleIndex := cellIndex(i, j, cols, rows) |
| 165 | + |
| 166 | + if possibleIndex == -1 { |
| 167 | + return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex) |
| 168 | + } |
| 169 | + return grid[possibleIndex], nil |
| 170 | +} |
| 171 | + |
| 172 | +func removeWalls(a *cell, b *cell) { |
| 173 | + x := a.col - b.col |
| 174 | + |
| 175 | + if x == 1 { |
| 176 | + a.walls[3] = false |
| 177 | + b.walls[1] = false |
| 178 | + } else if x == -1 { |
| 179 | + a.walls[1] = false |
| 180 | + b.walls[3] = false |
| 181 | + } |
| 182 | + |
| 183 | + y := a.row - b.row |
| 184 | + |
| 185 | + if y == 1 { |
| 186 | + a.walls[0] = false |
| 187 | + b.walls[2] = false |
| 188 | + } else if y == -1 { |
| 189 | + a.walls[2] = false |
| 190 | + b.walls[0] = false |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +func run() { |
| 195 | + // unsiged integers, because easier parsing error checks. |
| 196 | + // We must convert these to intergers, as done below... |
| 197 | + uScreenWidth, uScreenHeight, uWallSize := parseArgs() |
| 198 | + |
| 199 | + var ( |
| 200 | + // In pixels |
| 201 | + // Defualt is 800x800x40 = 20x20 wallgrid |
| 202 | + screenWidth = int(uScreenWidth) |
| 203 | + screenHeight = int(uScreenHeight) |
| 204 | + wallSize = int(uWallSize) |
| 205 | + |
| 206 | + frames = 0 |
| 207 | + second = time.Tick(time.Second) |
| 208 | + |
| 209 | + grid = []*cell{} |
| 210 | + cols = screenWidth / wallSize |
| 211 | + rows = screenHeight / wallSize |
| 212 | + currentCell = new(cell) |
| 213 | + backTrackStack = stack.NewStack(1) |
| 214 | + ) |
| 215 | + |
| 216 | + // Set game FPS manually |
| 217 | + fps := time.Tick(time.Second / 60) |
| 218 | + |
| 219 | + cfg := pixelgl.WindowConfig{ |
| 220 | + Title: "Pixel Rocks! - Maze example", |
| 221 | + Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)), |
| 222 | + } |
| 223 | + |
| 224 | + win, err := pixelgl.NewWindow(cfg) |
| 225 | + if err != nil { |
| 226 | + panic(err) |
| 227 | + } |
| 228 | + |
| 229 | + grid, backTrackStack, currentCell = setupMaze(cols, rows) |
| 230 | + |
| 231 | + gridIMDraw := imdraw.New(nil) |
| 232 | + |
| 233 | + for !win.Closed() { |
| 234 | + if win.JustReleased(pixelgl.KeyR) { |
| 235 | + fmt.Println("R pressed") |
| 236 | + grid, backTrackStack, currentCell = setupMaze(cols, rows) |
| 237 | + } |
| 238 | + |
| 239 | + win.Clear(colornames.Gray) |
| 240 | + gridIMDraw.Clear() |
| 241 | + |
| 242 | + for i := range grid { |
| 243 | + grid[i].Draw(gridIMDraw, wallSize) |
| 244 | + } |
| 245 | + |
| 246 | + // step 1 |
| 247 | + // Make the initial cell the current cell and mark it as visited |
| 248 | + currentCell.visited = true |
| 249 | + currentCell.hightlight(gridIMDraw, wallSize) |
| 250 | + |
| 251 | + // step 2.1 |
| 252 | + // If the current cell has any neighbours which have not been visited |
| 253 | + // Choose a random unvisited cell |
| 254 | + nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows) |
| 255 | + if nextCell != nil && !nextCell.visited { |
| 256 | + // step 2.2 |
| 257 | + // Push the current cell to the stack |
| 258 | + backTrackStack.Push(currentCell) |
| 259 | + |
| 260 | + // step 2.3 |
| 261 | + // Remove the wall between the current cell and the chosen cell |
| 262 | + |
| 263 | + removeWalls(currentCell, nextCell) |
| 264 | + |
| 265 | + // step 2.4 |
| 266 | + // Make the chosen cell the current cell and mark it as visited |
| 267 | + nextCell.visited = true |
| 268 | + currentCell = nextCell |
| 269 | + } else if backTrackStack.Len() > 0 { |
| 270 | + currentCell = backTrackStack.Pop().(*cell) |
| 271 | + } |
| 272 | + |
| 273 | + gridIMDraw.Draw(win) |
| 274 | + win.Update() |
| 275 | + <-fps |
| 276 | + updateFPSDisplay(win, &cfg, &frames, grid, second) |
| 277 | + } |
| 278 | +} |
| 279 | + |
| 280 | +// Parses the maze arguments, all of them are optional. |
| 281 | +// Uses uint as implicit error checking :) |
| 282 | +func parseArgs() (uint, uint, uint) { |
| 283 | + var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.") |
| 284 | + var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.") |
| 285 | + var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.") |
| 286 | + |
| 287 | + flag.Parse() |
| 288 | + |
| 289 | + // If these aren't default values AND if they're not the same values. |
| 290 | + // We should warn the user that the maze will look funny. |
| 291 | + if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 { |
| 292 | + if *mazeWidthPtr != *mazeHeightPtr { |
| 293 | + fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr) |
| 294 | + fmt.Println("Maze will look funny because the maze size is bond to the window size!") |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr |
| 299 | +} |
| 300 | + |
| 301 | +func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) { |
| 302 | + *frames++ |
| 303 | + select { |
| 304 | + case <-second: |
| 305 | + win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid))) |
| 306 | + *frames = 0 |
| 307 | + default: |
| 308 | + } |
| 309 | + |
| 310 | +} |
| 311 | + |
| 312 | +func main() { |
| 313 | + if debug { |
| 314 | + defer profile.Start().Stop() |
| 315 | + } |
| 316 | + pixelgl.Run(run) |
| 317 | +} |
0 commit comments