Scope

In this article, as a sequel to Genetic algorithm for icon generation in Visual Basic, we'll see how genetic algorithms can be applied to solve problems. The approach is particularly suited to a requirement where we don't have a prior model (or, an ideal target to reach), but know the optimal parameters that tend to a solution. In other words, we'll discuss Genetic Algorithms (GA) in the solutions optimization context. The language in which we'll develop a sample application will be Visual Basic .NET again.

Introduction

In the last article we considered how, starting from a given graphical model, it's possible to generate a set of random and chaotic solutions that will evolve towards our target through the constant selection of the best samples from each set, crossing their information to create new champions, perhaps better. Though those are interesting ways to explore GAs, when we discuss those coded to grow towards a strict model have the obvious need of a starting complete and ideal solution. That means if we don't have a final result to match each solution we create, we won't have any way to evolve beyond chaos, closing our algorithm in an infinite loop of random samples, because they won't be oriented to any specificity.

Obviously, a GA must always have a target, in one way or another. However, a monolithic model isn't always necessary, but after we have defined the optimal conditions we expect, we let the algorithm find solutions that meet those requirements (or, the optimal solution to a given problem). There are many practical applications for that, regarding many fields of human knowledge. But since we learn better when we play with something, we'll see here an example of a Visual Basic GA oriented to solve a 2D maze, trying, using previously seen functionalities, to not only join two points in space, but to do it in one of the better ways, in other words searching for the shorter path.

A brief recap

Let's review the main concepts to refresh your memory.

A GA is so named because it tries to mimic those biological processes at the core of species evolution. We could say it attempts to create a computer model able to auto-tune itself to a specific context, generating individuals (solutions to a given problem) that will be used to evaluate a real goal of a given objective. The problem to be solved is the digital equivalent of the biological context in which individuals move, obeying the "fittest's survive" tenet. In the previous article I've used the example of a world filled with predators, in which the fastest inhabitants (more apt to an efficient flight) or the more pugnacious (mode apt to an efficient fight) represents the best candidates to survive, if compared with slower o weaker samples. In the same way, if we have a digital target (equivalent of the base parameter of biological survival), we could tell about "apt individuals" in the measure in which potential solutions get nearer to the given objective. Among them, in the same way nature does, we'll try to identify the best solutions to get them to reproduce, generating new solutions that will bear their parent's peculiarities. A quick sample about it has been shown in the first article. Given a certain graphical icon (an array of bytes that produces an image), starting from a fixed number of chaotic solutions, in each generation the best solutions are those akin to the model at which they aim.

The biological inspiration of GA passes through its terminology, too. In it, we'll speak of:

All those concepts are valid in a digital world and must be implemented in some way to speak about a real GA-based solution, or a solution which can autoevolve. The developer must define each one of them, in theoretical and practical means to deal with each phase.

Optimizing a solution

In principle, when dealing with the case of image replication too, when taling about GAs we could always think about optimization. In fact, each GA case is, ultimately, the search for the best solution, whenever it refers to reach the ideal target or falling in a satisfying local optimum. With the term "local optimum", we indicate the inability to progress further ta a given precision rate, due to certain population characteristics that, after recombination, doesn't produce improvements. A method to prevent stagnation is possibly applied through mutation, using which we introduce, on a random basis, changes in the newly created solutions, to maintain a little constant of chaos in transmitting information from a generation to the next one.

Despite that, we could always refer to the concept of optimization, it becomes more evident when applied to a logistical problem. Let's suppose we are inside a maze that provides several ways to reach the destination. In it we could move as the internal topography allows and it is possible to repeat a path or never reach the destination. Essentially, in such a labyrinth there are many ways to reach a solution, but a solution that will be better compared to others exists, too. In such a place, the best road is the one that leads us from the start point to the end in a straight way, in the least steps possible (in other words, the optimal solution). A GA could help us to spot that path, without previously knowing what the possibilities are. We note it isn't just about finding a way to arrive, but to test its validity, considering what will be better.

Base structure

To be able to think about a labyrinth, we must have a labyrinth. Here we'll see the basis I've used in my example, to briefly explain what will be the ground on which we will work. As usual, at the end of the article you'll find a link to download the source code of the example that I invite you to consult for a detailed view about those aspects that won't be treated in this article. Running the sample code, we'll see a window like this:

The program will create two windows with 20 cells per side that will be used as a graphical representation on the elements constituting the user-defined labyrinth (on the left) and the one that will show the found solutions (on the right). In the left panel, the user can click each cell a given number of times. The following is a short table to explain what each click will produce on a cell:

To be able to test the program quickly, I've implemented two buttons to save and load a previously created labyrinth. In the source code's package, I've made available the "test_maze.txt" file that is the one we use in this example. Loading that file, we'll obtain the following labyrinth:

We can immediately see there are many possible solutions between the start and end point. Before getting into the GA part, I will spend some words about the creation of the labyrinth, to explain its underlying logic. That will be useful for a better understanding of the algorithm itself.

Implementing a labyrinth in VB

Our labyrinth has a base made by a bidimensional byte array, named sourceMaze(). Each cell of it represents an array position, in the form (X,Y). Each of those bytes is set to a value that represents the current state of a specific cell. Those values are associated with an enumeration, visible in the Globals.vb file:

  1. Module Globals
  2. Public Const MAZESIZE As Byte = 20
  3. Public Const MAZESIDE As Byte = 15
  4. Public Enum CELLSTATE
  5. Free = 0
  6. Wall = 1
  7. Start = 2
  8. Finish = 3
  9. Valid = 4
  10. FakeWall = 5
  11. End Enum
  12. End Module

There is an additional value compared to the values discussed above, namely FakeWall = 5. We will talk about it soon, when we'll analyze how the solutions are created.

During the Form load, a call to the routine that draws the labyrinths is made. Drawing operations are done using MazeCell controls that are PictureBox extensions created to contain data from the data array as in the following:

  1. Private Sub GenerateMaze(container As Panel)
  2. Dim _TOP As Integer = 0
  3. Dim _LEFT As Integer = 0
  4. For _Y As Integer = 0 To MAZESIZE - 1
  5. For _X As Integer = 0 To MAZESIZE - 1
  6. Dim pb As New MazeCell
  7. With pb
  8. .Name = container.Name & _Y.ToString("00") & _X.ToString("00")
  9. .Top = _TOP
  10. .Left = _LEFT
  11. .Width = MAZESIDE
  12. .Height = MAZESIDE
  13. .BackColor = Color.Black
  14. .BorderStyle = BorderStyle.FixedSingle
  15. .Cursor = Cursors.Hand
  16. .MatX = _X
  17. .MatY = _Y
  18. AddHandler pb.Click, AddressOf MazeClickCell
  19. End With
  20. _LEFT += MAZESIDE
  21. container.Controls.Add(pb)
  22. Next
  23. _LEFT = 0
  24. _TOP += MAZESIDE
  25. Next
  26. End Sub
GenerateMaze() needs an argument of type Panel that represents the owner control in which we'll generate our strcture. Next, with two nested loops, cells are created. They will be bound with the sourceMaze array during MazeClickCell event, defined as:
  1. Private Sub MazeClickCell(sender As Object, e As EventArgs)
  2. Dim pb As MazeCell = CType(sender, MazeCell)
  3. pb.CellStatus += 1
  4. If pb.CellStatus > CELLSTATE.Valid Then pb.CellStatus = CELLSTATE.Free
  5. sourceMaze(pb.MatX, pb.MatY) = pb.CellStatus
  6. pb.BackColor = pb.CellBrush
  7. End Sub
We can note MazeCell controls, in comparison with standard PictureBox, have additional properties, like MatX that identifies a cell's abscissa in the matrix, MatY that represents an ordinate, CellStatus that exposes a cell's content and CellBrush that, following the status of a cell, represents the cell itself with a color. The source code that realizes the MazeCell extension is the following:
  1. Public Class MazeCell
  2. Inherits PictureBox
  3. Dim _STATUS As Byte = 0
  4. Dim _MATX As Byte = 0
  5. Dim _MATY As Byte = 0
  6. Public Property CellStatus As Byte
  7. Get
  8. Return _STATUS
  9. End Get
  10. Set(value As Byte)
  11. _STATUS = value
  12. End Set
  13. End Property
  14. Public ReadOnly Property CellBrush As Color
  15. Get
  16. Select Case CellStatus
  17. Case CELLSTATE.Free
  18. Return Color.Black
  19. Case CELLSTATE.Valid
  20. Return Color.Violet
  21. Case CELLSTATE.Start
  22. Return Color.Green
  23. Case CELLSTATE.Finish
  24. Return Color.Red
  25. Case CELLSTATE.Wall
  26. Return Color.Gray
  27. Case CELLSTATE.FakeWall
  28. Return Color.Yellow
  29. End Select
  30. End Get
  31. End Property
  32. Public Property MatX As Byte
  33. Get
  34. Return _MATX
  35. End Get
  36. Set(value As Byte)
  37. _MATX = value
  38. End Set
  39. End Property
  40. Public Property MatY As Byte
  41. Get
  42. Return _MATY
  43. End Get
  44. Set(value As Byte)
  45. _MATY = value
  46. End Set
  47. End Property
  48. Public ReadOnly Property StringMatrix As String
  49. Get
  50. Return _MATX.ToString & ";" & _MATY.ToString
  51. End Get
  52. End Property
  53. End Class

The present structure allows the user to define his own base labyrinth, to be then passed as a base for the GA, or saving it for future reference. (I've implemented, but not optimized, basic routines to save and load labyrinth data, but I won't discuss them here, to focus on the topic's fundamentals.)

Genetic algorithm logics

Now that we have an operative context, we can start reasoning about the explorative possibilities of an automaton. We've mentioned above about the rules to be applied for each type of cell and consequently we know what the basic prequisites are that a generic explorative algorithm must possess (please note: generic, not genetic). We know it must start moving from the Green cell, trying to reach the Red one, without crossing Grey ones. All the Black cells can be explored/stepped over. We can think about a routine which, given a start point, evaluates the status of the four adjacent cells, to recursively cross those that will be considered free.

In other words, being solverBytes() and an array containing a given solution, a function like this will do:

  1. Private Function Solve(_X As Integer, _Y As Integer) As Boolean
  2. If solverBytes(_endPos(0), _endPos(1)) = CELLSTATE.Valid Then Return True
  3. If solverBytes(_X + 1, _Y) = CELLSTATE.Free Or solverBytes(_X + 1, _Y) = CELLSTATE.Start Or solverBytes(_X + 1, _Y) = CELLSTATE.Finish Then
  4. solverBytes(_X + 1, _Y) = CELLSTATE.Valid
  5. If Solve(_X + 1, _Y) Then Return True
  6. End If
  7. If solverBytes(_X - 1, _Y) = CELLSTATE.Free Or solverBytes(_X - 1, _Y) = CELLSTATE.Start Or solverBytes(_X - 1, _Y) = CELLSTATE.Finish Then
  8. solverBytes(_X - 1, _Y) = CELLSTATE.Valid
  9. If Solve(_X - 1, _Y) Then Return True
  10. End If
  11. If solverBytes(_X, _Y + 1) = CELLSTATE.Free Or solverBytes(_X, _Y + 1) = CELLSTATE.Start Or solverBytes(_X, _Y + 1) = CELLSTATE.Finish Then
  12. solverBytes(_X, _Y + 1) = CELLSTATE.Valid
  13. If Solve(_X, _Y + 1) Then Return True
  14. End If
  15. If solverBytes(_X, _Y - 1) = CELLSTATE.Free Or solverBytes(_X, _Y + 1) = CELLSTATE.Start Or solverBytes(_X, _Y + 1) = CELLSTATE.Finish Then
  16. solverBytes(_X, _Y - 1) = CELLSTATE.Valid
  17. If Solve(_X, _Y - 1) Then Return True
  18. End If
  19. Return False
  20. End Function

As can be noted, giving a start point ad [_X,_Y] coordinates results in looping through the function (and recursively into it) until all the cells that leads to the solution are crossed. In other words, the algorithm will explore the entire solution domain. Applying the function to our labyrinth, we will obtain a solution like this:

Obviously, that will be an encompassing solution, with the sole purpose of tracking down all possibilities. It is the most dispersive solution. Nonetheless, it could have its usefulness in a specific context (please note that, at a conceptual level, our function is a trivial graphical floodfill, given drawing margins), but that's not what we desire to obtain here. We are starting to see what the peculiarities are of a GA, what it must be able to do. In our case, it must try to solve a labyrinth, without trying to taking out a massive amount of space. So, generating individuals capable of solving our problem, we can introduce a kind of limiter.

We saw the limiter above, in the CELLSTATE enumeration, with the value of FakeWall = 5. When we'll generate a new individual, we'll introduce in it some "dirt", to alter the results of the Solve() function with fake walls, precisely, or walls that are not in the base labyrinth structure, having the same behavior of a standard wall.

To be spotted more easily, we'll draw them using the color Yellow. As said, those are arbitrary limiters that respond to the same impossability laws as traditional walls and are useful to delimiter, for each related individual, the solution domain. In fact, we can note that, where a Yellow wall is present, the exploration of the maze is blocked from that spot on. Such randomness allows us to generate a manifold population of individuals. It will contain elements that will solve the maze and elements unable to reach the exit (for example, think about a Yellow cell standing right in front of a Red one). Next, each solution will arrive to its conclusion in a given number of steps. There will be elements that will arrive to the Red cell in a more straight way, whereas another will follow more convoluted paths, being more dispersive.

A hypothetical solution in VB

Let's now see the code of the GASolver class that represents the single individual or solution. Please note it has a generic constructor, with a parameter that expects the array of the user-defined maze, to replicate in each solution the bytes representing walls, starting point and an end point that are essential prerequisites to solve the maze, since they are fixed. With some probability, one or more cells will take the FakeWall value, to add limiters where, in drawing the maze, the user has left free cells.

  1. Public Class GASolver
  2. Dim _startpos(1) As Byte
  3. Dim _endPos(1) As Byte
  4. Dim _isSolved As Boolean
  5. Dim solverBytes(MAZESIZE, MAZESIZE) As Byte
  6. Public ReadOnly Property StartPos As Byte()
  7. Get
  8. Return _startpos
  9. End Get
  10. End Property
  11. Public ReadOnly Property EndPos As Byte()
  12. Get
  13. Return _endPos
  14. End Get
  15. End Property
  16. Public ReadOnly Property GetBytes As Byte(,)
  17. Get
  18. Return solverBytes
  19. End Get
  20. End Property
  21. Public ReadOnly Property Steps As Integer
  22. Get
  23. Dim _steps As Integer = 0
  24. For _Y As Integer = 0 To MAZESIZE - 1
  25. For _X As Integer = 0 To MAZESIZE - 1
  26. If solverBytes(_X, _Y) = CELLSTATE.Valid Then _steps += 1
  27. Next
  28. Next
  29. Return _steps
  30. End Get
  31. End Property
  32. Public ReadOnly Property isSolved As Boolean
  33. Get
  34. Return Solve(_startpos(0), _startpos(1))
  35. End Get
  36. End Property
  37. Private Function Solve(_X As Integer, _Y As Integer) As Boolean
  38. If solverBytes(_endPos(0), _endPos(1)) = CELLSTATE.Valid Then Return True
  39. If solverBytes(_X + 1, _Y) = CELLSTATE.Free Or solverBytes(_X + 1, _Y) = CELLSTATE.Start Or solverBytes(_X + 1, _Y) = CELLSTATE.Finish Then
  40. solverBytes(_X + 1, _Y) = CELLSTATE.Valid
  41. If Solve(_X + 1, _Y) Then Return True
  42. End If
  43. If solverBytes(_X - 1, _Y) = CELLSTATE.Free Or solverBytes(_X - 1, _Y) = CELLSTATE.Start Or solverBytes(_X - 1, _Y) = CELLSTATE.Finish Then
  44. solverBytes(_X - 1, _Y) = CELLSTATE.Valid
  45. If Solve(_X - 1, _Y) Then Return True
  46. End If
  47. If solverBytes(_X, _Y + 1) = CELLSTATE.Free Or solverBytes(_X, _Y + 1) = CELLSTATE.Start Or solverBytes(_X, _Y + 1) = CELLSTATE.Finish Then
  48. solverBytes(_X, _Y + 1) = CELLSTATE.Valid
  49. If Solve(_X, _Y + 1) Then Return True
  50. End If
  51. If solverBytes(_X, _Y - 1) = CELLSTATE.Free Or solverBytes(_X, _Y + 1) = CELLSTATE.Start Or solverBytes(_X, _Y + 1) = CELLSTATE.Finish Then
  52. solverBytes(_X, _Y - 1) = CELLSTATE.Valid
  53. If Solve(_X, _Y - 1) Then Return True
  54. End If
  55. Return False
  56. End Function
  57. Public Sub New(bytes01(,) As Byte, bytes02(,) As Byte, startPos As Byte(), endPos As Byte())
  58. _startpos = startPos
  59. _endPos = endPos
  60. For _Y As Integer = 0 To MAZESIZE - 1
  61. For _X As Integer = 0 To MAZESIZE - 1
  62. If bytes01(_X, _Y) = CELLSTATE.Wall Then solverBytes(_X, _Y) = bytes01(_X, _Y)
  63. If bytes01(_X, _Y) = CELLSTATE.FakeWall Then solverBytes(_X, _Y) = bytes01(_X, _Y)
  64. Next
  65. Next
  66. For _Y As Integer = 0 To MAZESIZE - 1
  67. For _X As Integer = 0 To MAZESIZE - 1
  68. If bytes02(_X, _Y) = CELLSTATE.Wall Then solverBytes(_X, _Y) = bytes02(_X, _Y)
  69. If bytes02(_X, _Y) = CELLSTATE.FakeWall Then solverBytes(_X, _Y) = bytes02(_X, _Y)
  70. Next
  71. Next
  72. For _Y As Integer = 0 To MAZESIZE - 1
  73. For _X As Integer = 0 To MAZESIZE - 1
  74. If solverBytes(_X, _Y) <> CELLSTATE.Wall Then
  75. Randomize()
  76. If Int(Rnd() * 100) Mod 99 = 0 Then
  77. If Int(Rnd() * 100) Mod 99 = 0 Then
  78. solverBytes(_X, _Y) = CELLSTATE.FakeWall
  79. Else
  80. solverBytes(_X, _Y) = CELLSTATE.Free
  81. End If
  82. End If
  83. End If
  84. Next
  85. Next
  86. solverBytes(startPos(0), startPos(1)) = CELLSTATE.Start
  87. solverBytes(endPos(0), endPos(1)) = CELLSTATE.Finish
  88. End Sub
  89. Public Sub New(sourceBytes(,) As Byte)
  90. For _Y As Integer = 0 To MAZESIZE - 1
  91. For _X As Integer = 0 To MAZESIZE - 1
  92. If sourceBytes(_X, _Y) = CELLSTATE.Start Then _startpos(0) = _X : _startpos(1) = _Y
  93. If sourceBytes(_X, _Y) = CELLSTATE.Finish Then _endPos(0) = _X : _endPos(1) = _Y
  94. If sourceBytes(_X, _Y) = CELLSTATE.Free Then
  95. Randomize()
  96. If Int(Rnd() * 100) Mod 50 = 0 Then
  97. solverBytes(_X, _Y) = CELLSTATE.FakeWall
  98. Else
  99. solverBytes(_X, _Y) = CELLSTATE.Free
  100. End If
  101. Else
  102. solverBytes(_X, _Y) = sourceBytes(_X, _Y)
  103. End If
  104. Next
  105. Next
  106. End Sub
  107. End Class

A Solve() function is also present, to be able to determine if a solution can reach the exit cell, or if it fails somewhere else. There are other properties and functions that will be analyzed soon.

Solution's fitness

As we saw in the previous article, an essential part of a GA is the one that allows a determination of an adaptability score when confronted with a given problem. It goes without saying that the way in which that score will be calculated rests on the problem's tipology. Fitness is essential, because when we have a population made of a solution more or less close to our target, it allows the extraction of some champions with potential characterics we can desire to try to imrove the individual's generation. It is therefore evident that an error in designing a fitness calculator's function will be the foundation stone to build a failure in a GA realization.

In trying to limit problems laying on an eroneous fitness attribution to one or more solutions, deep problems analyzing is a requirement. In cases as an image replication is simpler, we know it is, at the core, an array of bytes ordered in a given way and the fitness of a solution passes by checking how many bytes are shared between our target and the solutions. When confronted with open problems, like the present one, a step forward is needed. Here we aren't interested in evaluating the affinity with a predefined model, but we want to discuss the potential effctiveness of a solution.

Simplifying, in the case of a maze, we are interested in the following two aspects, already mentioned previously:

  1. The solution must lead to a valid end, it must reach the destination
  2. The solution must be quick; having satisfied the prerequisite 1), it must be able to obtain it in the most neat way
We can say that, in our case, the fitness calculation passes for two properties. The first is to reach a goal, whereas the second is the number of steps taken, where the fewer there are, the greater is our solution's quality. To that means, the GASolver class implements two respective properties, isSolver, which returns a boolean value to understand that it is a specific individual that reaches the target and Steps that count the amount of valid cells in the solution itself.
Initialization and crossing-over

The initialization's concept follows simple rules that are about creating a first generation of solutions, using standard constructors. In the attached source code, the operation happens at the btnInit button's Click:

  1. Private Sub btnInit_Click(sender As Object, e As EventArgs) Handles btnInit.Click
  2. epochs = 0
  3. For _Y As Integer = 0 To MAZESIZE - 1
  4. For _X As Integer = 0 To MAZESIZE - 1
  5. If sourceMaze(_X, _Y) = CELLSTATE.Start Then _startPos(0) = _X : _startPos(1) = _Y
  6. If sourceMaze(_X, _Y) = CELLSTATE.Finish Then _endPos(0) = _X : _endPos(1) = _Y
  7. Next
  8. Next
  9. gaList = New List(Of GASolver)
  10. For ii As Integer = 0 To 999
  11. gaList.Add(New GASolver(sourceMaze))
  12. Next
  13. gaCursor = 0
  14. MazePaintItem(mazeDest)
  15. btnStart.Enabled = True
  16. btnStop.Enabled = False
  17. End Sub

We reset a counter that will be used to count the generations, then we save the cells that the user has defined as the start and end points. In a List (OF GASolver), that we will query via LINQ, we'll add the desired number of newly created GASolvers (in our case, 1000 units), to proced to their graphical representation. Each initial solution, as we saw, will be initialized by adding dummy delimiters into the spaces the user has kept clear, to create variety in the population.

By pressing the btnStart button, the seach for solutions will start. Note that we will extract, during each loop, those elements that solve the maze (ga.isSolved = True), ordered by the number of steps used to complete the task (ordered by ga.Steps Ascending). That way, we will be sure to extract the two best solutions at the moment. Those elements will be recombined, producing two new elements, following the rules we'll see in a moment. Lastly, ordering our list for a descending amount of steps, we can discard the two worst elements. Please note that we won't apply any discriminant on the solution's quality and it is possible to discard elements that arrivees at a valid solution, but having the flaw to be, among all solutions, the most dispersive ones.

Cross-over generates two new elements as explained using the four-argument constructor of the GASolver class, as follows:

  1. Public Sub New(bytes01(,) As Byte, bytes02(,) As Byte, startPos As Byte(), endPos As Byte())
  2. _startpos = startPos
  3. _endPos = endPos
  4. For _Y As Integer = 0 To MAZESIZE - 1
  5. For _X As Integer = 0 To MAZESIZE - 1
  6. If bytes01(_X, _Y) = CELLSTATE.Wall Then solverBytes(_X, _Y) = bytes01(_X, _Y)
  7. If bytes01(_X, _Y) = CELLSTATE.FakeWall Then solverBytes(_X, _Y) = bytes01(_X, _Y)
  8. Next
  9. Next
  10. For _Y As Integer = 0 To MAZESIZE - 1
  11. For _X As Integer = 0 To MAZESIZE - 1
  12. If bytes02(_X, _Y) = CELLSTATE.Wall Then solverBytes(_X, _Y) = bytes02(_X, _Y)
  13. If bytes02(_X, _Y) = CELLSTATE.FakeWall Then solverBytes(_X, _Y) = bytes02(_X, _Y)
  14. Next
  15. Next
  16. For _Y As Integer = 0 To MAZESIZE - 1
  17. For _X As Integer = 0 To MAZESIZE - 1
  18. If solverBytes(_X, _Y) <> CELLSTATE.Wall Then
  19. Randomize()
  20. If Int(Rnd() * 100) Mod 99 = 0 Then
  21. If Int(Rnd() * 100) Mod 99 = 0 Then
  22. solverBytes(_X, _Y) = CELLSTATE.FakeWall
  23. Else
  24. solverBytes(_X, _Y) = CELLSTATE.Free
  25. End If
  26. End If
  27. End If
  28. Next
  29. Next
  30. solverBytes(startPos(0), startPos(1)) = CELLSTATE.Start
  31. solverBytes(endPos(0), endPos(1)) = CELLSTATE.Finish
  32. End Sub

Essentially, it requires two arrays, referred to the bytes of the elements to be recombined and two pairs of coordinates, namely the start and exit points of the maze.

Looping through the two matrices, a matrix is generated, putting in it the combination of Wall and FakeWall types. A Wall cell will be the same between the two, but a FakeWall will possibily differ. If the two solutions come to a valid end, in other words the FakeWall from both matrices can be applied without affect the solution, then it is made better by removing a potentially free cell to be considered valid steps. When the process ends, an additional mutation loop calculates the eventual transformation of non-Wall cells in Wall or FakeWall cells. The newly created solution will then have the same Walls as the parents, the same start/end point, but the virtual walls from both, plus some mutation (if they occur).

The logic is simple: the thing that determines a more economic solution, is the freedom of movement. The more the free cells are, the greater the solution's domain will be, and consequently the steps towards the exit point. In the described way, we apply a progressive limiter towards a basic path that will be constituted by the sum of the limiters that, in past generations, had represented and helped that objective.

Making the algorithm run, alternating generations, in a few hundreds of epochs we'll have the optimal solution, or the solution that leads from the start point to the end without "indecisions". At the same time, the progressive discarding of poor solutions, in favor of those with higher performances, will lead to an entire population of acceptable solutions.

In the preceding example, it's possible to see that after about 14000 generations of solutions, the result is really close to the optimal one. There is room to improve, and proceeding though epochs it's possible to reach better results, though slow due to the recombination of nearly-ideal solutions (that can count almost exclusively on mutation to improve further). Lastly, please note that, with a given number of potential solutions, the path chosen by the algorithm will be affected by the elaborations themselves. For example, if a mutation inhibits a certain path, the algorithm will focus on the remaining solutions. So, we won't obtain the ultimately better overall solution, but the better depending on the current context and the agents at our disposal, miming at a conceptual level what happens in biology too.

Source Code
Demo
References