Skip to content

Commit

Permalink
Implement Origin Shift algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
romandykyi committed Jun 30, 2024
1 parent cd71249 commit b65f7d3
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
75 changes: 75 additions & 0 deletions Labyrinthian/DirectedMaze.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Collections.Generic;

namespace Labyrinthian
{
/// <summary>
/// A wrapper of the <see cref="Labyrinthian.Maze" /> class which supports directed edges.
/// </summary>
/// <remarks>
/// Note that this class does not track changes to the maze edges made outside of it.
/// </remarks>
public class DirectedMaze
{
private readonly Dictionary<MazeEdge, bool> _edgesDirections;

/// <summary>
/// Get an original maze.
/// </summary>
public Maze Maze { get; private set; }

/// <summary>
/// Gets a dictionary which defines a direction of each maze edge.
/// </summary>
/// <remarks>
/// If an edge has a value <see langword="false" />, then direction is Cell1 -> Cell2,
/// otherwise it's Cell2 -> Cell1.
/// </remarks>
public IReadOnlyDictionary<MazeEdge, bool> EdgesDirections => _edgesDirections;

/// <summary>
/// Construct a directed maze based on a maze.
/// Note that existing edges will be ignored and will not appear in
/// <see cref="EdgesDirections" /> unless added within this class.
/// </summary>
/// <param name="maze">A maze to be based on.</param>
public DirectedMaze(Maze maze)
{
_edgesDirections = new Dictionary<MazeEdge, bool>();
Maze = maze;
}

/// <summary>
/// Add/update a relation cell1 -> cell2 and carve a passage between the cells in the maze.
/// </summary>
/// <param name="cell1">A start cell.</param>
/// <param name="cell2">An end cell.</param>
public void ConnectCells(MazeCell cell1, MazeCell cell2)
{
// Ensure that cell1.Index < cell2.Index
var edge = MazeEdge.GetMinMax(cell1, cell2);
// Select the right direction
_edgesDirections[edge] = cell1.Index > cell2.Index;

Maze.ConnectCells(cell1, cell2);
}

/// <summary>
/// Remove a relation cell1 -> cell2 and create a wall between the cells in the maze.
/// </summary>
/// <remarks>
/// If relation cell2 -> cell1 exists instead of cell1 -> cell2, then nothing will change.
/// </remarks>
/// <param name="cell1">A start cell.</param>
/// <param name="cell2">An end cell.</param>
public void DisconnectCells(MazeCell cell1, MazeCell cell2)
{
var edge = MazeEdge.GetMinMax(cell1, cell2);
if (_edgesDirections.TryGetValue(edge, out bool value) &&
value == cell1.Index > cell2.Index)
{
_edgesDirections.Remove(edge);
Maze.BlockCells(cell1, cell2);
}
}
}
}
13 changes: 13 additions & 0 deletions Labyrinthian/Generation/IDirectedGraphGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Labyrinthian.Generation
{
/// <summary>
/// An interface for generators which use directed graphs.
/// </summary>
public interface IDirectedGraphGenerator
{
/// <summary>
/// Get a directed maze.
/// </summary>
public DirectedMaze DirectedMaze { get; }
}
}
119 changes: 119 additions & 0 deletions Labyrinthian/Generation/OriginShiftGeneration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;

namespace Labyrinthian.Generation
{
/// <summary>
/// Maze generator that uses Origin Shift algorithm.
/// Algorithm's author: https://github.com/CaptainLuma.
/// </summary>
public class OriginShiftGeneration : MazeGenerator, IDirectedGraphGenerator
{
/// <summary>
/// Gets the max algorithm's iterations number.
/// If the value is negative, generator generates a maze forever.
/// </summary>
public int MaxIterations { get; private set; }

public DirectedMaze DirectedMaze { get; private set; }

/// <summary>
/// Construct a generator with a default seed.
/// </summary>
/// <param name="maze">Maze used for generation.</param>
/// <param name="initialCell">Cell that will be current at the start of generation(optional).</param>
/// <param name="maxIterations">
/// Max algorithm's iterations number. If negative, maze doesn't stop generating,
/// if <see langword="null"/> then a default number of iterations is used based on the number of maze cells.
/// </param>
public OriginShiftGeneration(Maze maze, int? maxIterations = null, MazeCell? initialCell = null) :
this(maze, Environment.TickCount, maxIterations, initialCell)
{ }

/// <summary>
/// Construct a generator with specified seed.
/// </summary>
/// <param name="maze">Maze used for generation.</param>
/// <param name="seed">Seed for random numbers generator.</param>
/// <param name="initialCell">Cell that will be current at the start of generation(optional).</param>
/// <param name="maxIterations">
/// Max algorithm's iterations number. If negative, maze doesn't stop generating,
/// if <see langword="null"/> then a default number of iterations is used based on the number of maze cells.
/// </param>
public OriginShiftGeneration(Maze maze, int seed, int? maxIterations = null, MazeCell? initialCell = null) :
base(maze, seed, initialCell, true)
{
DirectedMaze = new DirectedMaze(Maze);
MaxIterations = maxIterations ?? Maze.Cells.Length * 10;
}

private void ConnectToOrigin(MazeCell origin)
{
// Use BFS to connect all cells to the origin
Queue<MazeCell> cells = new Queue<MazeCell>();
cells.Enqueue(origin);
var visited = new MarkedCells(Maze, false);
visited[origin] = true;
while (cells.Count > 0)
{
var currentCell = cells.Dequeue();
foreach (var neighbor in currentCell.Neighbors)
{
if (!visited[neighbor])
{
visited[neighbor] = true;
cells.Enqueue(neighbor);

DirectedMaze.ConnectCells(neighbor, currentCell);
}
}
}
}

protected override IEnumerable<Maze> Generation()
{
// https://github.com/CaptainLuma/New-Maze-Generating-Algorithm

// Choose the initial cell and mark it as origin
SelectedCell ??= GetRandomCell();
// Make a starting perfect maze
ConnectToOrigin(SelectedCell);

yield return Maze;

for (int i = 0; i < MaxIterations || MaxIterations < 0; i++)
{
// Have the origin node, point to a random neighboring node
int neighborIndex = Rnd.Next(0, SelectedCell.Neighbors.Length);
var selectedNeighbor = SelectedCell.Neighbors[neighborIndex];
DirectedMaze.ConnectCells(SelectedCell, selectedNeighbor);

// That neigboring node becomes the new origin node
SelectedCell = selectedNeighbor;

// Have the new origin node point nowhere
foreach (var neighbor in SelectedCell.Neighbors)
{
DirectedMaze.DisconnectCells(SelectedCell, neighbor);
}

yield return Maze;
}
}

public override Maze Generate()
{
if (MaxIterations < 0)
{
throw new InvalidOperationException("To prevent an infinite loop, Generate cannot be called when MaxIterations is set to a negative number.");
}

return base.Generate();
}

public override string ToString()
{
return "Origin Shift";
}
}
}

0 comments on commit b65f7d3

Please sign in to comment.