-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add maze generator community example
- Loading branch information
faiface
committed
May 30, 2017
1 parent
781c44f
commit 4b7553c
Showing
5 changed files
with
443 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.