Skip to content

Commit 4b7553c

Browse files
author
faiface
committed
add maze generator community example
1 parent 781c44f commit 4b7553c

5 files changed

Lines changed: 443 additions & 0 deletions

File tree

examples/community/maze/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 stephen
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

examples/community/maze/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Maze generator in Go
2+
3+
Created by [Stephen Chavez](https://github.com/redragonx)
4+
5+
This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel
6+
7+
I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.
8+
9+
Controls: Press 'R' to restart the maze.
10+
11+
Optional command-line arguments: `go run ./maze-generator.go`
12+
- `-w` sets the maze's width in pixels.
13+
- `-h` sets the maze's height in pixels.
14+
- `-c` sets the maze cell's size in pixels.
15+
16+
Code based on the Recursive backtracker algorithm.
17+
- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
18+
19+
![Screenshot](screenshot.png)
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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+
}
13.8 KB
Loading

0 commit comments

Comments
 (0)