318 lines
7.8 KiB
Go
318 lines
7.8 KiB
Go
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)
|
|
}
|