316 lines
9.8 KiB
C#
316 lines
9.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
|
|
namespace MazePathfinding
|
|
{
|
|
public class Cell
|
|
{
|
|
public int Row { get; set; }
|
|
public int Col { get; set; }
|
|
public int X { get; set; }
|
|
public int Y { get; set; }
|
|
public bool[] Walls { get; set; } = new bool[4]; // top, right, bottom, left
|
|
public bool Visited { get; set; }
|
|
|
|
public Cell(int row, int col, int cellSize)
|
|
{
|
|
Row = row;
|
|
Col = col;
|
|
X = col * cellSize;
|
|
Y = row * cellSize;
|
|
for (int i = 0; i < 4; i++) Walls[i] = true;
|
|
Visited = false;
|
|
}
|
|
|
|
public void Draw(Graphics g, int cellSize)
|
|
{
|
|
if (Visited)
|
|
{
|
|
using (var brush = new SolidBrush(Color.White))
|
|
g.FillRectangle(brush, X, Y, cellSize, cellSize);
|
|
}
|
|
|
|
using (var pen = new Pen(Color.Black, 2))
|
|
{
|
|
if (Walls[0]) g.DrawLine(pen, X, Y, X + cellSize, Y);
|
|
if (Walls[1]) g.DrawLine(pen, X + cellSize, Y, X + cellSize, Y + cellSize);
|
|
if (Walls[2]) g.DrawLine(pen, X + cellSize, Y + cellSize, X, Y + cellSize);
|
|
if (Walls[3]) g.DrawLine(pen, X, Y + cellSize, X, Y);
|
|
}
|
|
}
|
|
|
|
public void Highlight(Graphics g, Color color, int cellSize)
|
|
{
|
|
using (var brush = new SolidBrush(color))
|
|
g.FillRectangle(brush, X, Y, cellSize, cellSize);
|
|
}
|
|
}
|
|
|
|
public class MazeForm : Form
|
|
{
|
|
private const int WIDTH = 600;
|
|
private int rows = 20;
|
|
private int cellSize;
|
|
private Cell[,] grid;
|
|
private Cell start;
|
|
private Cell end;
|
|
private List<Cell> path = new List<Cell>();
|
|
private HashSet<Cell> openSet = new HashSet<Cell>();
|
|
private System.Windows.Forms.Timer timer;
|
|
private bool isSearching = false;
|
|
|
|
public MazeForm()
|
|
{
|
|
Text = "A* Maze Pathfinding - Press SPACE to solve, UP/DOWN to resize";
|
|
ClientSize = new Size(WIDTH, WIDTH);
|
|
DoubleBuffered = true;
|
|
KeyPreview = true;
|
|
FormBorderStyle = FormBorderStyle.FixedSingle;
|
|
MaximizeBox = false;
|
|
|
|
cellSize = WIDTH / rows;
|
|
CreateMaze(rows);
|
|
|
|
timer = new System.Windows.Forms.Timer { Interval = 16 };
|
|
timer.Tick += (s, e) => Invalidate();
|
|
timer.Start();
|
|
|
|
this.KeyDown += MazeForm_KeyDown;
|
|
this.Paint += MazeForm_Paint;
|
|
}
|
|
|
|
private void CreateMaze(int r)
|
|
{
|
|
rows = r;
|
|
cellSize = WIDTH / rows;
|
|
grid = new Cell[rows, rows];
|
|
|
|
for (int i = 0; i < rows; i++)
|
|
for (int j = 0; j < rows; j++)
|
|
grid[i, j] = new Cell(i, j, cellSize);
|
|
|
|
GenerateMaze(grid[0, 0]);
|
|
start = grid[0, 0];
|
|
end = grid[rows - 1, rows - 1];
|
|
path.Clear();
|
|
openSet.Clear();
|
|
}
|
|
|
|
private void GenerateMaze(Cell current)
|
|
{
|
|
var stack = new Stack<Cell>();
|
|
current.Visited = true;
|
|
var random = new Random();
|
|
|
|
while (true)
|
|
{
|
|
var neighbors = new List<Cell>();
|
|
int r = current.Row, c = current.Col;
|
|
|
|
if (r > 0 && !grid[r - 1, c].Visited) neighbors.Add(grid[r - 1, c]);
|
|
if (r < rows - 1 && !grid[r + 1, c].Visited) neighbors.Add(grid[r + 1, c]);
|
|
if (c > 0 && !grid[r, c - 1].Visited) neighbors.Add(grid[r, c - 1]);
|
|
if (c < rows - 1 && !grid[r, c + 1].Visited) neighbors.Add(grid[r, c + 1]);
|
|
|
|
if (neighbors.Count > 0)
|
|
{
|
|
var nextCell = neighbors[random.Next(neighbors.Count)];
|
|
stack.Push(current);
|
|
RemoveWalls(current, nextCell);
|
|
nextCell.Visited = true;
|
|
current = nextCell;
|
|
}
|
|
else if (stack.Count > 0)
|
|
{
|
|
current = stack.Pop();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RemoveWalls(Cell a, Cell b)
|
|
{
|
|
int dx = a.Col - b.Col;
|
|
int dy = a.Row - b.Row;
|
|
|
|
if (dx == 1)
|
|
{
|
|
a.Walls[3] = false;
|
|
b.Walls[1] = false;
|
|
}
|
|
else if (dx == -1)
|
|
{
|
|
a.Walls[1] = false;
|
|
b.Walls[3] = false;
|
|
}
|
|
|
|
if (dy == 1)
|
|
{
|
|
a.Walls[0] = false;
|
|
b.Walls[2] = false;
|
|
}
|
|
else if (dy == -1)
|
|
{
|
|
a.Walls[2] = false;
|
|
b.Walls[0] = false;
|
|
}
|
|
}
|
|
|
|
private async void MazeForm_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (isSearching) return;
|
|
|
|
if (e.KeyCode == Keys.Space)
|
|
{
|
|
isSearching = true;
|
|
path = await Task.Run(() => AStar(start, end));
|
|
isSearching = false;
|
|
Invalidate();
|
|
}
|
|
else if (e.KeyCode == Keys.Up)
|
|
{
|
|
CreateMaze(rows + 1);
|
|
Invalidate();
|
|
}
|
|
else if (e.KeyCode == Keys.Down && rows > 5)
|
|
{
|
|
CreateMaze(rows - 1);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
private int Heuristic(Cell a, Cell b)
|
|
{
|
|
return Math.Abs(a.Row - b.Row) + Math.Abs(a.Col - b.Col);
|
|
}
|
|
|
|
private List<Cell> AStar(Cell startCell, Cell endCell)
|
|
{
|
|
var openSetQueue = new SortedSet<(int f, int count, Cell cell)>(
|
|
Comparer<(int f, int count, Cell cell)>.Create((a, b) =>
|
|
{
|
|
int cmp = a.f.CompareTo(b.f);
|
|
return cmp != 0 ? cmp : a.count.CompareTo(b.count);
|
|
}));
|
|
|
|
int count = 0;
|
|
openSetQueue.Add((0, count, startCell));
|
|
var cameFrom = new Dictionary<Cell, Cell>();
|
|
var gScore = new Dictionary<Cell, int>();
|
|
var fScore = new Dictionary<Cell, int>();
|
|
|
|
for (int i = 0; i < rows; i++)
|
|
for (int j = 0; j < rows; j++)
|
|
{
|
|
gScore[grid[i, j]] = int.MaxValue;
|
|
fScore[grid[i, j]] = int.MaxValue;
|
|
}
|
|
|
|
gScore[startCell] = 0;
|
|
fScore[startCell] = Heuristic(startCell, endCell);
|
|
|
|
openSet.Clear();
|
|
openSet.Add(startCell);
|
|
|
|
while (openSetQueue.Count > 0)
|
|
{
|
|
var current = openSetQueue.Min.cell;
|
|
openSetQueue.Remove(openSetQueue.Min);
|
|
openSet.Remove(current);
|
|
|
|
if (current == endCell)
|
|
{
|
|
var pathResult = new List<Cell>();
|
|
while (cameFrom.ContainsKey(current))
|
|
{
|
|
current = cameFrom[current];
|
|
pathResult.Add(current);
|
|
}
|
|
openSet.Clear();
|
|
return pathResult;
|
|
}
|
|
|
|
foreach (var neighbor in GetNeighbors(current))
|
|
{
|
|
int tempG = gScore[current] + 1;
|
|
if (tempG < gScore[neighbor])
|
|
{
|
|
cameFrom[neighbor] = current;
|
|
gScore[neighbor] = tempG;
|
|
fScore[neighbor] = tempG + Heuristic(neighbor, endCell);
|
|
|
|
if (!openSet.Contains(neighbor))
|
|
{
|
|
count++;
|
|
openSetQueue.Add((fScore[neighbor], count, neighbor));
|
|
openSet.Add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
Invoke(new Action(() => Invalidate()));
|
|
}
|
|
catch { }
|
|
Thread.Sleep(100);
|
|
}
|
|
|
|
openSet.Clear();
|
|
return new List<Cell>();
|
|
}
|
|
|
|
private List<Cell> GetNeighbors(Cell cell)
|
|
{
|
|
var neighbors = new List<Cell>();
|
|
int r = cell.Row, c = cell.Col;
|
|
|
|
if (!cell.Walls[0] && r > 0) neighbors.Add(grid[r - 1, c]);
|
|
if (!cell.Walls[1] && c < rows - 1) neighbors.Add(grid[r, c + 1]);
|
|
if (!cell.Walls[2] && r < rows - 1) neighbors.Add(grid[r + 1, c]);
|
|
if (!cell.Walls[3] && c > 0) neighbors.Add(grid[r, c - 1]);
|
|
|
|
return neighbors;
|
|
}
|
|
|
|
private void MazeForm_Paint(object sender, PaintEventArgs e)
|
|
{
|
|
var g = e.Graphics;
|
|
g.Clear(Color.White);
|
|
|
|
for (int i = 0; i < rows; i++)
|
|
for (int j = 0; j < rows; j++)
|
|
grid[i, j].Draw(g, cellSize);
|
|
|
|
foreach (var cell in openSet)
|
|
cell.Highlight(g, Color.LimeGreen, cellSize);
|
|
|
|
foreach (var cell in path)
|
|
cell.Highlight(g, Color.Yellow, cellSize);
|
|
|
|
start.Highlight(g, Color.Blue, cellSize);
|
|
end.Highlight(g, Color.Red, cellSize);
|
|
|
|
// Redraw walls over highlights
|
|
for (int i = 0; i < rows; i++)
|
|
for (int j = 0; j < rows; j++)
|
|
grid[i, j].Draw(g, cellSize);
|
|
}
|
|
|
|
[STAThread]
|
|
static void Main()
|
|
{
|
|
Application.EnableVisualStyles();
|
|
Application.SetCompatibleTextRenderingDefault(false);
|
|
Application.Run(new MazeForm());
|
|
}
|
|
}
|
|
} |