Skip to content

Commit

Permalink
add maze generator community example
Browse files Browse the repository at this point in the history
  • Loading branch information
faiface committed May 30, 2017
1 parent 781c44f commit 4b7553c
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 0 deletions.
21 changes: 21 additions & 0 deletions examples/community/maze/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 stephen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
19 changes: 19 additions & 0 deletions examples/community/maze/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Maze generator in Go

Created by [Stephen Chavez](https://github.com/redragonx)

This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel

I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.

Controls: Press 'R' to restart the maze.

Optional command-line arguments: `go run ./maze-generator.go`
- `-w` sets the maze's width in pixels.
- `-h` sets the maze's height in pixels.
- `-c` sets the maze cell's size in pixels.

Code based on the Recursive backtracker algorithm.
- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker

![Screenshot](screenshot.png)
317 changes: 317 additions & 0 deletions examples/community/maze/maze-generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
package main

// Code based on the Recursive backtracker algorithm.
// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
// See https://youtu.be/HyK_Q5rrcr4 as an example
// YouTube example ported to Go for the Pixel library.

// Created by Stephen Chavez

import (
"crypto/rand"
"errors"
"flag"
"fmt"
"math/big"
"time"

"github.com/faiface/pixel"
"github.com/faiface/pixel/examples/community/maze/stack"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/pixelgl"

"github.com/pkg/profile"
"golang.org/x/image/colornames"
)

var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
var debug = false

type cell struct {
walls [4]bool // Wall order: top, right, bottom, left

row int
col int
visited bool
}

func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
drawCol := c.col * wallSize // x
drawRow := c.row * wallSize // y

imd.Color = colornames.White
if c.walls[0] {
// top line
imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
imd.Line(3)
}
if c.walls[1] {
// right Line
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
imd.Line(3)
}
if c.walls[2] {
// bottom line
imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
imd.Line(3)
}
if c.walls[3] {
// left line
imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
imd.Line(3)
}
imd.EndShape = imdraw.SharpEndShape

if c.visited {
imd.Color = visitedColor
imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
imd.Rectangle(0)
}
}

func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
neighbors := []*cell{}
j := c.row
i := c.col

top, _ := getCellAt(i, j-1, cols, rows, grid)
right, _ := getCellAt(i+1, j, cols, rows, grid)
bottom, _ := getCellAt(i, j+1, cols, rows, grid)
left, _ := getCellAt(i-1, j, cols, rows, grid)

if top != nil && !top.visited {
neighbors = append(neighbors, top)
}
if right != nil && !right.visited {
neighbors = append(neighbors, right)
}
if bottom != nil && !bottom.visited {
neighbors = append(neighbors, bottom)
}
if left != nil && !left.visited {
neighbors = append(neighbors, left)
}

if len(neighbors) == 0 {
return nil, errors.New("We checked all cells...")
}
return neighbors, nil
}

func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
neighbors, err := c.GetNeighbors(grid, cols, rows)
if neighbors == nil {
return nil, err
}
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
if err != nil {
panic(err)
}
randomIndex := nBig.Int64()
return neighbors[randomIndex], nil
}

func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
x := c.col * wallSize
y := c.row * wallSize

imd.Color = hightlightColor
imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
imd.Rectangle(0)
}

func newCell(col int, row int) *cell {
newCell := new(cell)
newCell.row = row
newCell.col = col

for i := range newCell.walls {
newCell.walls[i] = true
}
return newCell
}

// Creates the inital maze slice for use.
func initGrid(cols, rows int) []*cell {
grid := []*cell{}
for j := 0; j < rows; j++ {
for i := 0; i < cols; i++ {
newCell := newCell(i, j)
grid = append(grid, newCell)
}
}
return grid
}

func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
// Make an empty grid
grid := initGrid(cols, rows)
backTrackStack := stack.NewStack(len(grid))
currentCell := grid[0]

return grid, backTrackStack, currentCell
}

func cellIndex(i, j, cols, rows int) int {
if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
return -1
}
return i + j*cols
}

func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
possibleIndex := cellIndex(i, j, cols, rows)

if possibleIndex == -1 {
return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
}
return grid[possibleIndex], nil
}

func removeWalls(a *cell, b *cell) {
x := a.col - b.col

if x == 1 {
a.walls[3] = false
b.walls[1] = false
} else if x == -1 {
a.walls[1] = false
b.walls[3] = false
}

y := a.row - b.row

if y == 1 {
a.walls[0] = false
b.walls[2] = false
} else if y == -1 {
a.walls[2] = false
b.walls[0] = false
}
}

func run() {
// unsiged integers, because easier parsing error checks.
// We must convert these to intergers, as done below...
uScreenWidth, uScreenHeight, uWallSize := parseArgs()

var (
// In pixels
// Defualt is 800x800x40 = 20x20 wallgrid
screenWidth = int(uScreenWidth)
screenHeight = int(uScreenHeight)
wallSize = int(uWallSize)

frames = 0
second = time.Tick(time.Second)

grid = []*cell{}
cols = screenWidth / wallSize
rows = screenHeight / wallSize
currentCell = new(cell)
backTrackStack = stack.NewStack(1)
)

// Set game FPS manually
fps := time.Tick(time.Second / 60)

cfg := pixelgl.WindowConfig{
Title: "Pixel Rocks! - Maze example",
Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
}

win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}

grid, backTrackStack, currentCell = setupMaze(cols, rows)

gridIMDraw := imdraw.New(nil)

for !win.Closed() {
if win.JustReleased(pixelgl.KeyR) {
fmt.Println("R pressed")
grid, backTrackStack, currentCell = setupMaze(cols, rows)
}

win.Clear(colornames.Gray)
gridIMDraw.Clear()

for i := range grid {
grid[i].Draw(gridIMDraw, wallSize)
}

// step 1
// Make the initial cell the current cell and mark it as visited
currentCell.visited = true
currentCell.hightlight(gridIMDraw, wallSize)

// step 2.1
// If the current cell has any neighbours which have not been visited
// Choose a random unvisited cell
nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
if nextCell != nil && !nextCell.visited {
// step 2.2
// Push the current cell to the stack
backTrackStack.Push(currentCell)

// step 2.3
// Remove the wall between the current cell and the chosen cell

removeWalls(currentCell, nextCell)

// step 2.4
// Make the chosen cell the current cell and mark it as visited
nextCell.visited = true
currentCell = nextCell
} else if backTrackStack.Len() > 0 {
currentCell = backTrackStack.Pop().(*cell)
}

gridIMDraw.Draw(win)
win.Update()
<-fps
updateFPSDisplay(win, &cfg, &frames, grid, second)
}
}

// Parses the maze arguments, all of them are optional.
// Uses uint as implicit error checking :)
func parseArgs() (uint, uint, uint) {
var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")

flag.Parse()

// If these aren't default values AND if they're not the same values.
// We should warn the user that the maze will look funny.
if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
if *mazeWidthPtr != *mazeHeightPtr {
fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
fmt.Println("Maze will look funny because the maze size is bond to the window size!")
}
}

return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
}

func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
*frames++
select {
case <-second:
win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
*frames = 0
default:
}

}

func main() {
if debug {
defer profile.Start().Stop()
}
pixelgl.Run(run)
}
Binary file added examples/community/maze/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4b7553c

Please sign in to comment.