Skip to content

Instantly share code, notes, and snippets.

@pjmagee
Created January 10, 2024 23:55
Show Gist options
  • Select an option

  • Save pjmagee/8ec574b7181ae0c77cf3762786748fd4 to your computer and use it in GitHub Desktop.

Select an option

Save pjmagee/8ec574b7181ae0c77cf3762786748fd4 to your computer and use it in GitHub Desktop.
void Main()
{
var g = new Graph();
var p = new Player();
p.Location = g.Edges[3];
var destination = g.Nodes[0];
PathFinder finder = new PathFinder();
var path = finder.FindPath(g, p.Location, destination);
new
{
Start = p.Location,
End = destination,
Path = path
}
.Dump();
}
public class Graph
{
public List<Node> Nodes { get; } = new();
public List<Edge> Edges { get; } = new();
public Graph()
{
var context = new Context(){ Name = "Starting zone" };
var n1 = new Node() { Name = "N1", Context = context };
var n2 = new Node() { Name = "N2", Context = context };
var n3 = new Node() { Name = "N3", Context = context };
var n4 = new Node() { Name = "N4", Context = context };
var n5 = new Node() { Name = "N5", Context = context };
var e12 = new Edge() { From = n1, To = n2, Context = context, IsBidirectional = true };
var e23 = new Edge() { From = n2, To = n3, Context = context, IsBidirectional = true };
var e34 = new Edge() { From = n3, To = n4, Context = context, IsBidirectional = true };
var e45 = new Edge() { From = n4, To = n5, Context = context, IsBidirectional = true };
var e54 = new Edge() { From = n5, To = n4, Context = context, IsBidirectional = true };
Nodes.AddRange(new[] { n1, n2, n3, n4, n5 });
Edges.AddRange(new[] { e12, e23, e34, e54 });
}
}
/// <summary>
/// Context about the Location, is it a building, an area, etc.
/// A context can be shared by nodes and edges (A location)
/// </summary>
public class Context
{
public string Name { get; set; }
}
public class Player
{
public Location? Location { get; set; }
}
public class PathFinder
{
public List<Location> FindPath(Graph graph, Location start, Location goal)
{
var queue = new Queue<Location>();
var visited = new HashSet<Location>();
var cameFrom = new Dictionary<Location, Location>();
if (start == null || goal == null)
{
throw new ArgumentNullException("Start or goal location is null.");
}
queue.Enqueue(start);
visited.Add(start);
cameFrom[start] = null; // The start location came from nowhere.
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current.Equals(goal))
{
return ReconstructPath(cameFrom, current);
}
foreach (var neighbor in GetNeighbors(graph, current, visited))
{
if (!visited.Contains(neighbor))
{
queue.Enqueue(neighbor);
visited.Add(neighbor);
cameFrom[neighbor] = current;
}
}
}
return null; // Path not found
}
private IEnumerable<Location> GetNeighbors(Graph graph, Location current, HashSet<Location> visited)
{
var neighbors = new List<Location>();
foreach (var edge in graph.Edges)
{
if(edge.IsBidirectional && !edge.IsBlocked)
{
if (edge.From == current && !visited.Contains(edge.To) || edge.To == current && !visited.Contains(edge.From))
{
neighbors.Add(edge);
}
}
else if(!edge.IsBidirectional && !edge.IsBlocked)
{
if (edge.From == current && !visited.Contains(edge.To))
{
neighbors.Add(edge);
}
}
}
if (current is Edge e)
{
if (!visited.Contains(e.From))
{
neighbors.Add(e.From); // Add the node where the edge starts
}
if (!visited.Contains(e.To))
{
neighbors.Add(e.To); // Add the node where the edge ends
}
}
return neighbors;
}
private List<Location> ReconstructPath(Dictionary<Location, Location> cameFrom, Location? current)
{
var path = new List<Location>();
while (current != null)
{
path.Insert(0, current);
current = cameFrom.TryGetValue(current, out var previous) ? previous : null;
}
return path;
}
}
static object ToDump(object input)
{
if (input is Context c)
{
return c.Name;
}
if (input is Node n)
{
return n.Name;
}
if (input is Edge e)
{
return e.Name;
}
if(input is List<Location> l)
{
return l.Select(l => l);
}
return input;
}
public class Location
{
public Context Context { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public string Type => this.GetType().Name;
}
/// <summary>
/// A node is similar to an Edge, except can have Child Nodes.
/// An Edge, cannot have Child edges.
/// </summary>
public class Node : Location, IEquatable<Node>
{
/// <summary>
/// Optional edges (more for organisation and structure purposes)
/// </summary>
public List<Edge> Edges { get; set; } = new();
public bool Equals(Node? other) => Name.Equals(other?.Name);
public override string ToString() => $"{Type}:{Name}";
}
/// <summary>
/// An edge is a type of location that connects two other locations together
/// Similar to a Graph of Nodes/Vertices and Edges/Connections
/// The difference is that, a player can also be located at an Edge
/// An edge in this case, can be anything from a path, ladder, street or even another building, but provides semantics to how two Nodes are related.
/// </summary>
public class Edge : Location, IEquatable<Edge>
{
/// <summary>
/// Where this edge begins
/// </summary>
public Location From { get; init; }
/// <summary>
/// It is possible for an edge to lead to another edge
/// Generally, an Edge would link two 'Nodes' however, it's possible that an Edge could lead to another Edge
/// </summary>
public Location? To { get; init; }
/// <summary>
/// Sometimes, an Edge may be blocked due to various factors: Player level, skill, environment, quest, dynamic event etc.
/// </summary>
public bool IsBlocked { get; init; }
public bool IsBidirectional { get; init; } = false;
public override string ToString() => Name;
public override string Name
{
get
{
if (IsBidirectional)
{
return $"{From.Name} <-> {To.Name}";
}
return $"{From} -> {To}";
}
}
public bool Equals(Edge? other) => Name.Equals(other?.Name);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment