/// <summary> /// Solves the Tower of Hanoi problem recursively. /// </summary> /// <param name="discNumber"> /// The current disc number counted from 1. The number is also /// identical to the size of the (sub-)tower to be solved. /// </param> /// <param name="source"> /// The source/initial tower in form of a <see cref="Stack{T}"/>. /// </param> /// <param name="temporary"> /// The tower for temporary operations in form of a /// <see cref="Stack{T}"/>. /// </param> /// <param name="destination"> /// The destination/goal tower in form of a <see cref="Stack{T}"/>. /// </param> /// <exception cref="ArgumentException"> /// Thrown when <paramref name="discNumber"/> is smaller than 1 or /// any of the stack references (<paramref name="source"/>, /// <paramref name="temporary"/>, <paramref name="destination"/>) /// is null. /// </exception> /// <remarks> /// The essential part to understand the algorithm is to realize that /// it is the target/goal tower that varies according to the /// sub-problem. /// At least some previous knowledge of recursive methods is /// absolutely required or you might not take the leap at all. /// If you have played the puzzle on paper for towers of 1 to 3 discs /// you may intuitively - thinking ahead - have moved discs /// in similar constellations differently and may fail to see the /// generic pattern and why behind it. /// For a 1-disc tower it is trivial - you directly move from /// the start peg to the goal peg. /// For a 2-disc tower you (hopefully) are not going to move /// top disc 1 directly to goal but to the temporary peg instead /// as you are anticipating that you could then move disc 2 /// directly to its final destination, the goal peg. /// Finally you move disc 1 from current peg (temporary) to goal. /// For a 3-disc tower you should realize that it makes sense to /// first get the 2-disc "sub-tower" out of the way by erecting it /// on the temporary peg. Then you can simply move disc 3 to the /// final destination, the goal peg. Now the problem is reduced /// again to a 2-disc tower problem where the sub-tower is /// currently on the temporary peg and has to move to the goal peg. /// Hopefully you may start to see that it is only the "goal tower" /// that varies by the current needs. You move the current n - 1 /// tower of discs out of the way to be able to move the last or /// nth disc to the final destination. Then you move the n - 1 /// tower to the final destination. /// The more discs there are the more similar groups of steps /// there are that repeat over and over with varying source /// and destination pegs. /// </remarks> private void HanoiSolve(int discNumber, Stack <int> source, Stack <int> temporary, Stack <int> destination) { // Argument checking - discNumber must be >= 1 if (discNumber < 1) { throw new ArgumentException( $"Disc number must equal 1 or greater.", nameof(discNumber)); } // Null checks if (source == null || temporary == null || destination == null) { throw new ArgumentException( "Neither stack must be null."); } // Base case - 1 disc; recursion ends if (discNumber == 1) { // Move directly from source to goal var disc = source.Pop(); destination.Push(disc); // Cause a notification event to eg. draw and delay UI. DiscMoved?.Invoke(this, new DiscMovedEventArgs <int>(discNumber, source, destination)); } else { // Recursive procedure - first move all discs less than // current discNumber out of the way thus using temporary // as goal and goal as temporary storage. HanoiSolve(discNumber - 1, source, destination, temporary); // We freed the current disc on the current from-stack. // Now move the current disc from target to goal. var disc = source.Pop(); destination.Push(disc); // Cause a notification event to eg. draw and delay UI. DiscMoved?.Invoke(this, new DiscMovedEventArgs <int>(discNumber, source, destination)); // Finally move the "sub-tower" of (n - 1) parked discs // (ie. < discNumber) to the goal peg. // We know we are currently on temporary and have to go to // goal, using from as temporary as necessary. HanoiSolve(discNumber - 1, temporary, source, destination); } }
/// <summary> /// Runs the game simulation. /// </summary> /// <param name="numberOfDiscs"> /// The number of discs to use in the simulation. /// Defaults to 5. /// </param> /// <exception cref="ArgumentException"> /// Thrown when <paramref name="numberOfDiscs"/> is smaller than 1 or /// larger than the number of discs that could fit the current console /// window. /// </exception> public void Run(int numberOfDiscs = 5) { var discCandidates = OddNumbers(3) .Take(_maximumDiscs) .ToList(); var maximumDiscSelection = discCandidates .Where(n => n <= _maximumDiscWidth) .ToList(); WriteLine("~~~ Hanoi Stats ~~~"); WriteLine($"Console height: {_consoleHeight}"); WriteLine($"Console width: {_consoleWidth}"); WriteLine($"Maximum number of discs: {_maximumDiscs}"); WriteLine($"Width candidates: {String.Join(", ", discCandidates)}"); WriteLine($"Maximum disc width: {_maximumDiscWidth}"); WriteLine($"Maximum disc selection: {String.Join(", ", maximumDiscSelection)}"); WriteLine($"User-requested discs: {numberOfDiscs}"); if (numberOfDiscs < 1) { throw new ArgumentException( "The simulation must be run with a number of discs of 1 or greater.", nameof(numberOfDiscs)); } if (numberOfDiscs > _maximumDiscs) { throw new ArgumentException( $"Too many discs: A console of {_consoleWidth}x{_consoleHeight} can support a maximum of {_maximumDiscs} discs.", nameof(numberOfDiscs)); } Reset(); _userNumberOfDiscs = numberOfDiscs; var gameDiscs = maximumDiscSelection .Take(numberOfDiscs); WriteLine($"User discs: {String.Join(", ", gameDiscs)}"); WriteLine(); // Now put the selection of game discs on the source stack foreach (var gameDisc in gameDiscs.Reverse()) { _sourceStack.Push(gameDisc); } DiscMoved += Visualize; // Initial dummy disc move (no discs moved) to get the // starting point drawn to screen DiscMoved?.Invoke(this, DiscMovedEventArgs <int> .Empty); // Recursively solve to move from-stack to goal-stack HanoiSolve(_userNumberOfDiscs, _sourceStack, _temporaryStack, _destinationStack); DiscMoved -= Visualize; }