diff --git a/90 Tower/csharp/Tower.sln b/90 Tower/csharp/Tower.sln new file mode 100644 index 00000000..2c6e524d --- /dev/null +++ b/90 Tower/csharp/Tower.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tower", "tower\Tower.csproj", "{2E14FCD5-A52C-4292-A7F4-0C7E5780C962}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|x64.Build.0 = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Debug|x86.Build.0 = Debug|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|Any CPU.Build.0 = Release|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|x64.ActiveCfg = Release|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|x64.Build.0 = Release|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|x86.ActiveCfg = Release|Any CPU + {2E14FCD5-A52C-4292-A7F4-0C7E5780C962}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/90 Tower/csharp/Tower/Game.cs b/90 Tower/csharp/Tower/Game.cs new file mode 100644 index 00000000..7e0c7d0b --- /dev/null +++ b/90 Tower/csharp/Tower/Game.cs @@ -0,0 +1,78 @@ +using System; +using Tower.Models; +using Tower.Resources; +using Tower.UI; + +namespace Tower +{ + internal class Game + { + private readonly Towers _towers; + private readonly TowerDisplay _display; + private readonly int _optimalMoveCount; + private int _moveCount; + + public Game(int diskCount) + { + _towers = new Towers(diskCount); + _display = new TowerDisplay(_towers); + _optimalMoveCount = (1 << diskCount) - 1; + } + + public bool Play() + { + Console.Write(Strings.Instructions); + + Console.Write(_display); + + while (true) + { + if (!Input.TryReadNumber(Prompt.Disk, out int disk)) { return false; } + + if (!_towers.TryFindDisk(disk, out var from, out var message)) + { + Console.WriteLine(message); + continue; + } + + if (!Input.TryReadNumber(Prompt.Needle, out var to)) { return false; } + + if (!_towers.TryMoveDisk(from, to)) + { + Console.Write(Strings.IllegalMove); + continue; + } + + Console.Write(_display); + + var result = CheckProgress(); + if (result.HasValue) { return result.Value; } + } + } + + private bool? CheckProgress() + { + _moveCount++; + + if (_moveCount == 128) + { + Console.Write(Strings.TooManyMoves); + return false; + } + + if (_towers.Finished) + { + if (_moveCount == _optimalMoveCount) + { + Console.Write(Strings.Congratulations); + } + + Console.WriteLine(Strings.TaskFinished, _moveCount); + + return true; + } + + return default; + } + } +} diff --git a/90 Tower/csharp/Tower/Models/Needle.cs b/90 Tower/csharp/Tower/Models/Needle.cs new file mode 100644 index 00000000..1cd929fa --- /dev/null +++ b/90 Tower/csharp/Tower/Models/Needle.cs @@ -0,0 +1,33 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Tower.Models +{ + internal class Needle : IEnumerable + { + private readonly Stack _disks = new Stack(); + + public bool IsEmpty => _disks.Count == 0; + + public int Top => _disks.TryPeek(out var disk) ? disk : default; + + public bool TryPut(int disk) + { + if (_disks.Count == 0 || disk < _disks.Peek()) + { + _disks.Push(disk); + return true; + } + + return false; + } + + public bool TryGetTopDisk(out int disk) => _disks.TryPop(out disk); + + public IEnumerator GetEnumerator() => + Enumerable.Repeat(0, 7 - _disks.Count).Concat(_disks).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Models/Towers.cs b/90 Tower/csharp/Tower/Models/Towers.cs new file mode 100644 index 00000000..4b03b933 --- /dev/null +++ b/90 Tower/csharp/Tower/Models/Towers.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Tower.Resources; + +namespace Tower.Models +{ + internal class Towers : IEnumerable<(int, int, int)> + { + private static int[] _availableDisks = new[] { 15, 13, 11, 9, 7, 5, 3 }; + + private readonly Needle[] _needles = new[] { new Needle(), new Needle(), new Needle() }; + private readonly int _smallestDisk; + + public Towers(int diskCount) + { + foreach (int disk in _availableDisks.Take(diskCount)) + { + this[1].TryPut(disk); + _smallestDisk = disk; + } + } + + private Needle this[int i] => _needles[i-1]; + + public bool Finished => this[1].IsEmpty && this[2].IsEmpty; + + public bool TryFindDisk(int disk, out int needle, out string message) + { + needle = default; + message = default; + + if (disk < _smallestDisk) + { + message = Strings.DiskNotInPlay; + return false; + } + + for (needle = 1; needle <= 3; needle++) + { + if (this[needle].Top == disk) { return true; } + } + + message = Strings.DiskUnavailable; + return false; + } + + public bool TryMoveDisk(int from, int to) + { + if (!this[from].TryGetTopDisk(out var disk)) + { + throw new InvalidOperationException($"Needle {from} is empty"); + } + + if (this[to].TryPut(disk)) { return true; } + + this[from].TryPut(disk); + return false; + } + + public IEnumerator<(int, int, int)> GetEnumerator() => new TowersEnumerator(_needles); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private class TowersEnumerator : IEnumerator<(int, int, int)> + { + private readonly List> _enumerators; + + public TowersEnumerator(Needle[] needles) + { + _enumerators = needles.Select(n => n.GetEnumerator()).ToList(); + } + + public (int, int, int) Current => + (_enumerators[0].Current, _enumerators[1].Current, _enumerators[2].Current); + + object IEnumerator.Current => Current; + + public void Dispose() => _enumerators.ForEach(e => e.Dispose()); + + public bool MoveNext() => _enumerators.All(e => e.MoveNext()); + + public void Reset() => _enumerators.ForEach(e => e.Reset()); + } + } +} \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Program.cs b/90 Tower/csharp/Tower/Program.cs new file mode 100644 index 00000000..6bc9f107 --- /dev/null +++ b/90 Tower/csharp/Tower/Program.cs @@ -0,0 +1,27 @@ +using System; +using Tower.Resources; +using Tower.UI; + +namespace Tower +{ + class Program + { + static void Main(string[] args) + { + Console.Write(Strings.Title); + + do + { + Console.Write(Strings.Intro); + + if (!Input.TryReadNumber(Prompt.DiskCount, out var diskCount)) { return; } + + var game = new Game(diskCount); + + if (!game.Play()) { return; } + } while (Input.ReadYesNo(Strings.PlayAgainPrompt, Strings.YesNoPrompt)); + + Console.Write(Strings.Thanks); + } + } +} diff --git a/90 Tower/csharp/Tower/Resources/Congratulations.txt b/90 Tower/csharp/Tower/Resources/Congratulations.txt new file mode 100644 index 00000000..fb078fba --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Congratulations.txt @@ -0,0 +1,2 @@ + +Congratulations!! diff --git a/90 Tower/csharp/Tower/Resources/DiskCountPrompt.txt b/90 Tower/csharp/Tower/Resources/DiskCountPrompt.txt new file mode 100644 index 00000000..a45c3461 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskCountPrompt.txt @@ -0,0 +1 @@ +How many disks do you want to move (7 is max) \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskCountQuit.txt b/90 Tower/csharp/Tower/Resources/DiskCountQuit.txt new file mode 100644 index 00000000..b573854d --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskCountQuit.txt @@ -0,0 +1,2 @@ +All right, wise guy, if you can't play the game right, I'll +just take my puzzle and go home. So long. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskCountRetry.txt b/90 Tower/csharp/Tower/Resources/DiskCountRetry.txt new file mode 100644 index 00000000..c8ec4b10 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskCountRetry.txt @@ -0,0 +1 @@ +Sorry, but i can't do that job for you. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskNotInPlay.txt b/90 Tower/csharp/Tower/Resources/DiskNotInPlay.txt new file mode 100644 index 00000000..270ed0a5 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskNotInPlay.txt @@ -0,0 +1 @@ +That disk is not in play. Make another choice. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskPrompt.txt b/90 Tower/csharp/Tower/Resources/DiskPrompt.txt new file mode 100644 index 00000000..a7f127db --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskPrompt.txt @@ -0,0 +1 @@ +Which disk would you like to move \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskQuit.txt b/90 Tower/csharp/Tower/Resources/DiskQuit.txt new file mode 100644 index 00000000..56d19d5f --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskQuit.txt @@ -0,0 +1 @@ +Stop wasting my time. Go bother someone else. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskRetry.txt b/90 Tower/csharp/Tower/Resources/DiskRetry.txt new file mode 100644 index 00000000..1efe29b4 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskRetry.txt @@ -0,0 +1 @@ +Illegal entry... You may only type 3, 5, 7, 9, 11, 13, or 15. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/DiskUnavailable.txt b/90 Tower/csharp/Tower/Resources/DiskUnavailable.txt new file mode 100644 index 00000000..721ddccf --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/DiskUnavailable.txt @@ -0,0 +1 @@ +That disk is below another one. Make another choice. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/IllegalMove.txt b/90 Tower/csharp/Tower/Resources/IllegalMove.txt new file mode 100644 index 00000000..243c8d8f --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/IllegalMove.txt @@ -0,0 +1,3 @@ +You can't place a larger disk on top of a smaller one, +it might crush it! +Now then, \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/Instructions.txt b/90 Tower/csharp/Tower/Resources/Instructions.txt new file mode 100644 index 00000000..ca75537b --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Instructions.txt @@ -0,0 +1,11 @@ +In this program, we shall refer to disks by numerical code. +3 will represent the smallest disk, 5 the next size, +7 the next, and so on, up to 15. If you do the puzzle with +2 disks, their code names would be 13 and 15. With 3 disks +the code names would be 11, 13 and 15, etc. The needles +are numbered from left to right, 1 to 3. We will +startup with the disks on needle 1, and attempt to move them +to needle 3. + +Good luck! + diff --git a/90 Tower/csharp/Tower/Resources/Intro.txt b/90 Tower/csharp/Tower/Resources/Intro.txt new file mode 100644 index 00000000..e5345da6 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Intro.txt @@ -0,0 +1,7 @@ + +Towers of Hanoi puzzle. + +You must transfer the disks from the left to the right +tower, one at a time, never putting a larger dish on a +smaller disk. + diff --git a/90 Tower/csharp/Tower/Resources/NeedlePrompt.txt b/90 Tower/csharp/Tower/Resources/NeedlePrompt.txt new file mode 100644 index 00000000..59dcebe7 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/NeedlePrompt.txt @@ -0,0 +1 @@ +Place disk on which needle \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/NeedleQuit.txt b/90 Tower/csharp/Tower/Resources/NeedleQuit.txt new file mode 100644 index 00000000..e75e3d87 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/NeedleQuit.txt @@ -0,0 +1,2 @@ +I tried to warn you, but you wouldn't listen, +Bye bye, big shot. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/NeedleRetry.txt b/90 Tower/csharp/Tower/Resources/NeedleRetry.txt new file mode 100644 index 00000000..19701eec --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/NeedleRetry.txt @@ -0,0 +1,2 @@ +I'll assume you hit the wrong key this time. But watch it, +I only allow one mistake \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/PlayAgainPrompt.txt b/90 Tower/csharp/Tower/Resources/PlayAgainPrompt.txt new file mode 100644 index 00000000..3972e7bd --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/PlayAgainPrompt.txt @@ -0,0 +1,2 @@ + +Try again (Yes or No) \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/Strings.cs b/90 Tower/csharp/Tower/Resources/Strings.cs new file mode 100644 index 00000000..0cd8f6e1 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Strings.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Tower.Resources +{ + internal static class Strings + { + internal static string Congratulations => GetResource(); + internal static string DiskCountPrompt => GetResource(); + internal static string DiskCountQuit => GetResource(); + internal static string DiskCountRetry => GetResource(); + internal static string DiskNotInPlay => GetResource(); + internal static string DiskPrompt => GetResource(); + internal static string DiskQuit => GetResource(); + internal static string DiskRetry => GetResource(); + internal static string DiskUnavailable => GetResource(); + internal static string IllegalMove => GetResource(); + internal static string Instructions => GetResource(); + internal static string Intro => GetResource(); + internal static string NeedlePrompt => GetResource(); + internal static string NeedleQuit => GetResource(); + internal static string NeedleRetry => GetResource(); + internal static string PlayAgainPrompt => GetResource(); + internal static string TaskFinished => GetResource(); + internal static string Thanks => GetResource(); + internal static string Title => GetResource(); + internal static string TooManyMoves => GetResource(); + internal static string YesNoPrompt => GetResource(); + + private static string GetResource([CallerMemberName] string name = "") + { + var streamName = $"Tower.Resources.{name}.txt"; + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(streamName); + using var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + } +} diff --git a/90 Tower/csharp/Tower/Resources/TaskFinished.txt b/90 Tower/csharp/Tower/Resources/TaskFinished.txt new file mode 100644 index 00000000..e1ea1309 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/TaskFinished.txt @@ -0,0 +1,2 @@ + +You have performed the task in {0} moves. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/Thanks.txt b/90 Tower/csharp/Tower/Resources/Thanks.txt new file mode 100644 index 00000000..435d89c7 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Thanks.txt @@ -0,0 +1,2 @@ + +Thanks for the game! diff --git a/90 Tower/csharp/Tower/Resources/Title.txt b/90 Tower/csharp/Tower/Resources/Title.txt new file mode 100644 index 00000000..ae130577 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/Title.txt @@ -0,0 +1,5 @@ + Towers + Creative Computing Morristown, New Jersey + + + diff --git a/90 Tower/csharp/Tower/Resources/TooManyMoves.txt b/90 Tower/csharp/Tower/Resources/TooManyMoves.txt new file mode 100644 index 00000000..680803a5 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/TooManyMoves.txt @@ -0,0 +1,2 @@ +Sorry, but I have orders to stop if you make more than +128 moves. \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Resources/YesNoPrompt.txt b/90 Tower/csharp/Tower/Resources/YesNoPrompt.txt new file mode 100644 index 00000000..62612ce2 --- /dev/null +++ b/90 Tower/csharp/Tower/Resources/YesNoPrompt.txt @@ -0,0 +1,2 @@ + +'Yes' or 'No' please \ No newline at end of file diff --git a/90 Tower/csharp/Tower/Tower.csproj b/90 Tower/csharp/Tower/Tower.csproj new file mode 100644 index 00000000..c0de0594 --- /dev/null +++ b/90 Tower/csharp/Tower/Tower.csproj @@ -0,0 +1,12 @@ + + + + Exe + net5.0 + + + + + + + diff --git a/90 Tower/csharp/Tower/UI/Input.cs b/90 Tower/csharp/Tower/UI/Input.cs new file mode 100644 index 00000000..d1eca384 --- /dev/null +++ b/90 Tower/csharp/Tower/UI/Input.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace Tower.UI +{ + // Provides input methods which emulate the BASIC interpreter's keyboard input routines + internal static class Input + { + private static void Prompt(string text = "") => Console.Write($"{text}? "); + + internal static bool ReadYesNo(string prompt, string retryPrompt) + { + var response = ReadString(prompt); + + while (true) + { + if (response.Equals("No", StringComparison.InvariantCultureIgnoreCase)) { return false; } + if (response.Equals("Yes", StringComparison.InvariantCultureIgnoreCase)) { return true; } + response = ReadString(retryPrompt); + } + } + + internal static bool TryReadNumber(Prompt prompt, out int number) + { + var message = prompt.Message; + + for (int retryCount = 0; retryCount <= prompt.RetriesAllowed; retryCount++) + { + if (retryCount > 0) { Console.WriteLine(prompt.RetryMessage); } + + if (prompt.TryValidateResponse(ReadNumber(message), out number)) { return true; } + + if (!prompt.RepeatPrompt) { message = ""; } + } + + Console.WriteLine(prompt.QuitMessage); + + number = 0; + return false; + } + + private static float ReadNumber(string prompt) + { + Prompt(prompt); + + while (true) + { + var inputValues = ReadStrings(); + + if (TryParseNumber(inputValues[0], out var number)) + { + if (inputValues.Length > 1) + { + Console.WriteLine("!Extra input ingored"); + } + + return number; + } + } + } + + private static string ReadString(string prompt) + { + Prompt(prompt); + + var inputValues = ReadStrings(); + if (inputValues.Length > 1) + { + Console.WriteLine("!Extra input ingored"); + } + return inputValues[0]; + } + + private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries); + + private static bool TryParseNumber(string text, out float number) + { + if (float.TryParse(text, out number)) { return true; } + + Console.WriteLine("!Number expected - retry input line"); + number = default; + return false; + } + } +} diff --git a/90 Tower/csharp/Tower/UI/Prompt.cs b/90 Tower/csharp/Tower/UI/Prompt.cs new file mode 100644 index 00000000..bc83f7a6 --- /dev/null +++ b/90 Tower/csharp/Tower/UI/Prompt.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using static Tower.Resources.Strings; + +namespace Tower.UI +{ + internal class Prompt + { + public static Prompt DiskCount = + new(DiskCountPrompt, DiskCountRetry, DiskCountQuit, 1, 2, 3, 4, 5, 6, 7) { RetriesAllowed = 2 }; + + public static Prompt Disk = + new(DiskPrompt, DiskRetry, DiskQuit, 3, 5, 7, 9, 11, 13, 15) { RepeatPrompt = false }; + + public static Prompt Needle = new(NeedlePrompt, NeedleRetry, NeedleQuit, 1, 2, 3); + + private readonly HashSet _validValues; + + private Prompt(string prompt, string retryMessage, string quitMessage, params int[] validValues) + { + Message = prompt; + RetryMessage = retryMessage; + QuitMessage = quitMessage; + _validValues = validValues.ToHashSet(); + RetriesAllowed = 1; + RepeatPrompt = true; + } + + public string Message { get; } + public string RetryMessage { get; } + public string QuitMessage { get; } + public int RetriesAllowed { get; private set; } + public bool RepeatPrompt { get; private set; } + + public bool TryValidateResponse(float number, out int integer) + { + integer = (int)number; + return integer == number && _validValues.Contains(integer); + } + } +} \ No newline at end of file diff --git a/90 Tower/csharp/Tower/UI/TowerDisplay.cs b/90 Tower/csharp/Tower/UI/TowerDisplay.cs new file mode 100644 index 00000000..2493201f --- /dev/null +++ b/90 Tower/csharp/Tower/UI/TowerDisplay.cs @@ -0,0 +1,37 @@ +using System; +using System.Text; +using Tower.Models; + +namespace Tower.UI +{ + internal class TowerDisplay + { + private readonly Towers _towers; + + public TowerDisplay(Towers towers) + { + _towers = towers; + } + + public override string ToString() + { + var builder = new StringBuilder(); + + foreach (var row in _towers) + { + AppendTower(row.Item1); + AppendTower(row.Item2); + AppendTower(row.Item3); + builder.AppendLine(); + } + + return builder.ToString(); + + void AppendTower(int size) + { + var padding = 10 - size / 2; + builder.Append(' ', padding).Append('*', Math.Max(1, size)).Append(' ', padding); + } + } + } +} \ No newline at end of file