diff --git a/main.go b/main.go index 0cbb233..4bdeb82 100644 --- a/main.go +++ b/main.go @@ -265,7 +265,7 @@ func drawInfiniteGridLines(camera rl.Camera2D, canvasW float32, canvasH float32, func main() { rl.SetConfigFlags(rl.FlagWindowResizable) - rl.InitWindow(int32(800), int32(600), "A* Visualizer") + rl.InitWindow(int32(1200), int32(800), "A* Visualizer") defer rl.CloseWindow() scale := rl.GetWindowScaleDPI().X + 0.25 @@ -304,6 +304,11 @@ func main() { activeHeuristic := int32(0) heuristicDropdownOpen := false + mazeOptions := []string{"Recursive Divison", "Iterative DFS", "Cellular Automata"} + mazeOptionsText := strings.Join(mazeOptions, ";") + activeMaze := int32(0) + mazeDropdownOpen := false + cellSize := float32(25) lastMousePos := rl.NewVector2(-1, -1) @@ -512,7 +517,7 @@ func main() { if lastEvaluatedNode != -1 { startIndex := int(startPos.Y)*width + int(startPos.X) parents := astar.GetParents() - pathThickness := float32(10.0) / (camera.Zoom / 0.75) + pathThickness := float32(16.0) if pathThickness < 1 { pathThickness = 1 } @@ -605,12 +610,34 @@ func main() { // While a dropdown list is open it overlaps controls below; those widgets are handled // earlier in the frame and would otherwise steal the release-click. GuiLock skips input // for other controls; DropdownBox still handles input when its editMode is true. - if toolDropdownOpen || heuristicDropdownOpen { + if toolDropdownOpen || heuristicDropdownOpen || mazeDropdownOpen { rg.Lock() } + // Generate Maze Button + if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (260*scale), (180*scale), (30*scale)), "Generate Maze") { + astar.GenerateMaze(activeMaze) + astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating + lastEvaluatedNode = -1 + gridTypes := astar.GetGridTypes() + for i, gridType := range gridTypes { + // reset the map image + switch gridType { + case 0: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(240, 240, 240, 255)) + case 1: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 0, 0, 255)) + case 2: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(0, 255, 0, 255)) + case 3: + rl.ImageDrawPixel(mapImage, int32(i%width), int32(i/width), rl.NewColor(255, 0, 0, 255)) + } + } + tex.markFull() + } + // Reset Visualization Button - if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (200*scale), (180*scale), (30*scale)), "Reset Visualization") { + if rg.Button(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(120*scale)), (180*scale), (30*scale)), "Reset Visualization") { astar.ResetGrid(false) // keep grid types, otherwise it will delete the board before simulating lastEvaluatedNode = -1 gridTypes := astar.GetGridTypes() @@ -638,8 +665,8 @@ func main() { speedText = strconv.Itoa(speed/10) + "%" } speedLabel := "Speed: " + speedText - rg.Label(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(145*scale)), (180*scale), (30*scale)), speedLabel) - newSpeed := int(rg.SliderBar(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(120*scale)), (140*scale), (30*scale)), "", "", float32(speed), 0, 1000)) + rg.Label(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(185*scale)), (180*scale), (30*scale)), speedLabel) + newSpeed := int(rg.SliderBar(rl.NewRectangle(sidebarX+(10*scale), (screenHeight-(160*scale)), (140*scale), (30*scale)), "", "", float32(speed), 0, 1000)) if editModeSpeed { if newSpeed != speed { speed = newSpeed @@ -650,7 +677,7 @@ func main() { speed = newSpeed speedInputValue = strconv.Itoa(speed) } - if rg.TextBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(120*scale)), (30*scale), (30*scale)), &speedInputValue, 20, editModeSpeed) { + if rg.TextBox(rl.NewRectangle(sidebarX+(160*scale), (screenHeight-(160*scale)), (30*scale), (30*scale)), &speedInputValue, 20, editModeSpeed) { editModeSpeed = !editModeSpeed if editModeSpeed { speedInputValue = strconv.Itoa(speed) @@ -749,6 +776,9 @@ func main() { // Status Label rg.Label(rl.NewRectangle((10*scale), (screenHeight-(30*scale)), (canvasWidth-(20*scale)), (30*scale)), "Evaluated "+strconv.Itoa(astar.GetEvaluatedCells())+" cells in "+astar.GetTimeTaken().String()) + // FPS counter + rg.Label(rl.NewRectangle((10*scale), (0*scale), (canvasWidth-(20*scale)), (30*scale)), "FPS: "+strconv.Itoa(int(rl.GetFPS()))) + rg.Unlock() // Tool Selector (text must be "opt1;opt2;..." — raygui splits on ';' and needs 2+ items) @@ -765,6 +795,14 @@ func main() { } } + // Maze Selector + if !toolDropdownOpen && !heuristicDropdownOpen { + rg.Label(rl.NewRectangle(sidebarX+(10*scale), (195*scale), (180*scale), (30*scale)), "Maze:") + if rg.DropdownBox(rl.NewRectangle(sidebarX+(10*scale), (220*scale), (180*scale), (30*scale)), mazeOptionsText, &activeMaze, mazeDropdownOpen) { + mazeDropdownOpen = !mazeDropdownOpen + } + } + rl.EndDrawing() } } diff --git a/maze.go b/maze.go new file mode 100644 index 0000000..f38fced --- /dev/null +++ b/maze.go @@ -0,0 +1,211 @@ +package main + +import ( + "math/rand" + "time" +) + +func (a *AStar) GenerateMaze(mazeType int32) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + switch mazeType { + case 0: + a.GenerateRecursiveDivisionMaze(r) + case 1: + a.GenerateIterativeDFSMaze(r) + case 2: + a.GenerateCellularAutomataMaze(r) + } +} + +func (a *AStar) GenerateRecursiveDivisionMaze(r *rand.Rand) { + // 1. Start with an entirely empty grid + for i := range a.gridTypes { + a.gridTypes[i] = 0 + } + + // 2. Draw outer boundary walls + for x := 0; x < a.width; x++ { + a.gridTypes[x] = 1 + a.gridTypes[(a.height-1)*a.width+x] = 1 + } + for y := 0; y < a.height; y++ { + a.gridTypes[y*a.width] = 1 + a.gridTypes[y*a.width+(a.width-1)] = 1 + } + + // 3. Declare the recursive closure + var divide func(x, y, w, h int) + divide = func(x, y, w, h int) { + // Base case: room is too small to divide safely + if w <= 3 || h <= 3 { + return + } + + // Choose orientation based on proportions to keep rooms somewhat square + horizontal := h > w + if w == h { + horizontal = r.Intn(2) == 0 + } + + if horizontal { + // Horizontal Wall: Must be on an EVEN local Y coordinate + wallY := (r.Intn((h-2)/2) * 2) + 2 + // Gap (door): Must be on an ODD local X coordinate + gapX := (r.Intn((w-1)/2) * 2) + 1 + + for px := 0; px < w; px++ { + if px != gapX { + a.gridTypes[(y+wallY)*a.width+(x+px)] = 1 + } + } + // Recurse top and bottom + divide(x, y, w, wallY) + divide(x, y+wallY, w, h-wallY) + + } else { + // Vertical Wall: Must be on an EVEN local X coordinate + wallX := (r.Intn((w-2)/2) * 2) + 2 + // Gap (door): Must be on an ODD local Y coordinate + gapY := (r.Intn((h-1)/2) * 2) + 1 + + for py := 0; py < h; py++ { + if py != gapY { + a.gridTypes[(y+py)*a.width+(x+wallX)] = 1 + } + } + // Recurse left and right + divide(x, y, wallX, h) + divide(x+wallX, y, w-wallX, h) + } + } + + // Start the recursion on the inside of the boundary walls + divide(1, 1, a.width-2, a.height-2) +} + +func (a *AStar) GenerateIterativeDFSMaze(r *rand.Rand) { + // 1. Fill the entire grid with walls (type 1) + for i := range a.gridTypes { + a.gridTypes[i] = 1 + } + + // 2. The Stack (We use a Go slice instead of recursion to prevent Stack Overflow) + stack := make([]int, 0) + + // 3. Pick a random starting cell (MUST be odd coordinates for the step-by-two math) + startX := (r.Intn(a.width/2) * 2) + 1 + startY := (r.Intn(a.height/2) * 2) + 1 + + // Bounds check just in case + if startX >= a.width { + startX = a.width - 2 + } + if startY >= a.height { + startY = a.height - 2 + } + + startIdx := startY*a.width + startX + a.gridTypes[startIdx] = 0 // Carve the first floor + stack = append(stack, startIdx) + + // Directions for stepping by TWO (Up, Down, Left, Right) + dirs := [][]int{{0, -2}, {0, 2}, {-2, 0}, {2, 0}} + + // 4. The DFS Loop + for len(stack) > 0 { + // Pop the top of the stack + currentIdx := stack[len(stack)-1] + cx := currentIdx % a.width + cy := currentIdx / a.width + + // Find all valid, unvisited neighbors (distance 2) + validNeighbors := make([][]int, 0) + for _, dir := range dirs { + nx, ny := cx+dir[0], cy+dir[1] + // Check bounds + if nx > 0 && nx < a.width-1 && ny > 0 && ny < a.height-1 { + // If it's still a wall, we haven't visited it yet + if a.gridTypes[ny*a.width+nx] == 1 { + validNeighbors = append(validNeighbors, []int{nx, ny, dir[0], dir[1]}) + } + } + } + + if len(validNeighbors) > 0 { + // Pick a random valid neighbor + next := validNeighbors[r.Intn(len(validNeighbors))] + nx, ny, dx, dy := next[0], next[1], next[2], next[3] + + // Carve the neighbor (distance 2) + a.gridTypes[ny*a.width+nx] = 0 + + // Carve the wall BETWEEN current and neighbor (distance 1) + wallX, wallY := cx+(dx/2), cy+(dy/2) + a.gridTypes[wallY*a.width+wallX] = 0 + + // Push the neighbor to the stack + stack = append(stack, ny*a.width+nx) + } else { + // Backtrack! No valid neighbors, so pop it permanently + stack = stack[:len(stack)-1] + } + } +} + +func (a *AStar) GenerateCellularAutomataMaze(r *rand.Rand) { + // 1. Initial State: Fill with random noise (approx 45% walls) + for i := range a.gridTypes { + // Leave the edges as walls to contain the caves + x := i % a.width + y := i / a.width + if x == 0 || x == a.width-1 || y == 0 || y == a.height-1 { + a.gridTypes[i] = 1 + } else if r.Float32() < 0.45 { + a.gridTypes[i] = 1 + } else { + a.gridTypes[i] = 0 + } + } + + // 2. The Smoothing Passes (5 iterations is usually the sweet spot) + buffer := make([]byte, a.width*a.height) + + for step := 0; step < 5; step++ { + for y := 0; y < a.height; y++ { + for x := 0; x < a.width; x++ { + wallCount := 0 + + // Count the 8 surrounding neighbors + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + if dx == 0 && dy == 0 { + continue + } + nx, ny := x+dx, y+dy + + // Edges of the map count as walls + if nx < 0 || nx >= a.width || ny < 0 || ny >= a.height { + wallCount++ + } else if a.gridTypes[ny*a.width+nx] == 1 { + wallCount++ + } + } + } + + // The Automata Rules: + // If surrounded by walls, become a wall. + // If surrounded by empty space, become empty. + idx := y*a.width + x + if wallCount > 4 { + buffer[idx] = 1 + } else if wallCount < 4 { + buffer[idx] = 0 + } else { + buffer[idx] = a.gridTypes[idx] // Stays the same + } + } + } + // Copy the buffer back to the main grid for the next pass + copy(a.gridTypes, buffer) + } +}