diff --git a/30_Cube/README.md b/30_Cube/README.md index f99366a2..c220c8d4 100644 --- a/30_Cube/README.md +++ b/30_Cube/README.md @@ -16,3 +16,27 @@ http://www.vintage-basic.net/games.html #### Porting Notes (please note any difficulties or challenges in porting here) + +##### Randomization Logic + +The BASIC code uses an interesting technique for choosing the random coordinates for the mines. The first coordinate is +chosen like this: + +```basic +380 LET A=INT(3*(RND(X))) +390 IF A<>0 THEN 410 +400 LET A=3 +``` + +where line 410 is the start of a similar block of code for the next coordinate. The behaviour of `RND(X)` depends on the +value of `X`. If `X` is greater than zero then it returns a random value between 0 and 1. If `X` is zero it returns the +last random value generated, or 0 if no value has yet been generated. + +If `X` is 1, therefore, the first line above set `A` to 0, 1, or 2. The next 2 lines replace a 0 with a 3. The +replacement values varies for the different coordinates with the result that the random selection is biased towards a +specific set of points. If `X` is 0, the `RND` calls all return 0, so the coordinates are the known. It appears that +this technique was probably used to allow testing the game with a well-known set of locations for the mines. However, in +the code as it comes to us, the value of `X` is never set and is thus 0, so the mine locations are never randomized. + +The C# port implements the biased randomized mine locations, as seems to be the original intent, but includes a +command-line switch to enable the deterministic execution as well. diff --git a/30_Cube/csharp/Cube.csproj b/30_Cube/csharp/Cube.csproj index d3fe4757..3870320c 100644 --- a/30_Cube/csharp/Cube.csproj +++ b/30_Cube/csharp/Cube.csproj @@ -6,4 +6,12 @@ enable enable + + + + + + + + diff --git a/30_Cube/csharp/Game.cs b/30_Cube/csharp/Game.cs new file mode 100644 index 00000000..a06ec565 --- /dev/null +++ b/30_Cube/csharp/Game.cs @@ -0,0 +1,104 @@ +namespace Cube; + +internal class Game +{ + private const int _initialBalance = 500; + private readonly IEnumerable<(int, int, int)> _seeds = new List<(int, int, int)> + { + (3, 2, 3), (1, 3, 3), (3, 3, 2), (3, 2, 3), (3, 1, 3) + }; + private readonly (float, float, float) _startLocation = (1, 1, 1); + private readonly (float, float, float) _goalLocation = (3, 3, 3); + + private readonly IReadWrite _io; + private readonly IRandom _random; + + public Game(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + } + + public void Play() + { + _io.Write(Streams.Introduction); + + if (_io.ReadNumber("") != 0) + { + _io.Write(Streams.Instructions); + } + + PlaySeries(_initialBalance); + + _io.Write(Streams.Goodbye); + } + + private void PlaySeries(float balance) + { + while (true) + { + var wager = _io.ReadWager(balance); + + var gameWon = PlayGame(); + + if (wager.HasValue) + { + balance = gameWon ? (balance + wager.Value) : (balance - wager.Value); + if (balance <= 0) + { + _io.Write(Streams.Bust); + return; + } + _io.WriteLine(Formats.Balance, balance); + } + + if (_io.ReadNumber(Prompts.TryAgain) != 1) { return; } + } + } + + private bool PlayGame() + { + var mineLocations = _seeds.Select(seed => _random.NextLocation(seed)).ToHashSet(); + var currentLocation = _startLocation; + var prompt = Prompts.YourMove; + + while (true) + { + var newLocation = _io.Read3Numbers(prompt); + + if (!MoveIsLegal(currentLocation, newLocation)) { return Lose(Streams.IllegalMove); } + + currentLocation = newLocation; + + if (currentLocation == _goalLocation) { return Win(Streams.Congratulations); } + + if (mineLocations.Contains(currentLocation)) { return Lose(Streams.Bang); } + + prompt = Prompts.NextMove; + } + } + + private bool Lose(Stream text) + { + _io.Write(text); + return false; + } + + private bool Win(Stream text) + { + _io.Write(text); + return true; + } + + private bool MoveIsLegal((float, float, float) from, (float, float, float) to) + => (to.Item1 - from.Item1, to.Item2 - from.Item2, to.Item3 - from.Item3) switch + { + ( > 1, _, _) => false, + (_, > 1, _) => false, + (_, _, > 1) => false, + (1, 1, _) => false, + (1, _, 1) => false, + (_, 1, 1) => false, + _ => true + }; +} diff --git a/30_Cube/csharp/IOExtensions.cs b/30_Cube/csharp/IOExtensions.cs new file mode 100644 index 00000000..14f2a85e --- /dev/null +++ b/30_Cube/csharp/IOExtensions.cs @@ -0,0 +1,20 @@ +namespace Cube; + +internal static class IOExtensions +{ + internal static float? ReadWager(this IReadWrite io, float balance) + { + io.Write(Streams.Wager); + if (io.ReadNumber("") == 0) { return null; } + + var prompt = Prompts.HowMuch; + + while(true) + { + var wager = io.ReadNumber(prompt); + if (wager <= balance) { return wager; } + + prompt = Prompts.BetAgain; + } + } +} diff --git a/30_Cube/csharp/Program.cs b/30_Cube/csharp/Program.cs new file mode 100644 index 00000000..803a569a --- /dev/null +++ b/30_Cube/csharp/Program.cs @@ -0,0 +1,10 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; + +global using static Cube.Resources.Resource; + +using Cube; + +IRandom random = args.Contains("--non-random") ? new ZerosGenerator() : new RandomNumberGenerator(); + +new Game(new ConsoleIO(), random).Play(); diff --git a/30_Cube/csharp/README.md b/30_Cube/csharp/README.md index 4daabb5c..7bea3d88 100644 --- a/30_Cube/csharp/README.md +++ b/30_Cube/csharp/README.md @@ -1,3 +1,12 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Microsoft C#](https://docs.microsoft.com/en-us/dotnet/csharp/) + +#### Execution + +As noted in the main Readme file, the randomization code in the BASIC program has a switch (the variable `X`) that +allows the game to be run in a deterministic (non-random) mode. + +Running the C# port without command-line parameters will play the game with random mine locations. + +Running the port with a `--non-random` command-line switch will run the game with non-random mine locations. diff --git a/30_Cube/csharp/RandomExtensions.cs b/30_Cube/csharp/RandomExtensions.cs new file mode 100644 index 00000000..ac05108e --- /dev/null +++ b/30_Cube/csharp/RandomExtensions.cs @@ -0,0 +1,14 @@ +namespace Cube; + +internal static class RandomExtensions +{ + internal static (float, float, float) NextLocation(this IRandom random, (int, int, int) bias) + => (random.NextCoordinate(bias.Item1), random.NextCoordinate(bias.Item2), random.NextCoordinate(bias.Item3)); + + private static float NextCoordinate(this IRandom random, int bias) + { + var value = random.Next(3); + if (value == 0) { value = bias; } + return value; + } +} \ No newline at end of file diff --git a/30_Cube/csharp/Resources/Balance.txt b/30_Cube/csharp/Resources/Balance.txt new file mode 100644 index 00000000..1f6adffd --- /dev/null +++ b/30_Cube/csharp/Resources/Balance.txt @@ -0,0 +1 @@ +You now have {0} dollars. \ No newline at end of file diff --git a/30_Cube/csharp/Resources/Bang.txt b/30_Cube/csharp/Resources/Bang.txt new file mode 100644 index 00000000..1d924788 --- /dev/null +++ b/30_Cube/csharp/Resources/Bang.txt @@ -0,0 +1,4 @@ +******BANG****** +You lose! + + diff --git a/30_Cube/csharp/Resources/BetAgain.txt b/30_Cube/csharp/Resources/BetAgain.txt new file mode 100644 index 00000000..47c9fb8c --- /dev/null +++ b/30_Cube/csharp/Resources/BetAgain.txt @@ -0,0 +1 @@ +Tried to fool me; bet again \ No newline at end of file diff --git a/30_Cube/csharp/Resources/Bust.txt b/30_Cube/csharp/Resources/Bust.txt new file mode 100644 index 00000000..cd753d98 --- /dev/null +++ b/30_Cube/csharp/Resources/Bust.txt @@ -0,0 +1 @@ +You bust. diff --git a/30_Cube/csharp/Resources/Congratulations.txt b/30_Cube/csharp/Resources/Congratulations.txt new file mode 100644 index 00000000..3319c833 --- /dev/null +++ b/30_Cube/csharp/Resources/Congratulations.txt @@ -0,0 +1 @@ +Congratulations! diff --git a/30_Cube/csharp/Resources/Goodbye.txt b/30_Cube/csharp/Resources/Goodbye.txt new file mode 100644 index 00000000..0aa64192 --- /dev/null +++ b/30_Cube/csharp/Resources/Goodbye.txt @@ -0,0 +1,3 @@ +Tough luck! + +Goodbye. diff --git a/30_Cube/csharp/Resources/HowMuch.txt b/30_Cube/csharp/Resources/HowMuch.txt new file mode 100644 index 00000000..ff2bea20 --- /dev/null +++ b/30_Cube/csharp/Resources/HowMuch.txt @@ -0,0 +1 @@ +How much \ No newline at end of file diff --git a/30_Cube/csharp/Resources/IllegalMove.txt b/30_Cube/csharp/Resources/IllegalMove.txt new file mode 100644 index 00000000..ca8f96ba --- /dev/null +++ b/30_Cube/csharp/Resources/IllegalMove.txt @@ -0,0 +1,2 @@ + +Illegal move. You lose. diff --git a/30_Cube/csharp/Resources/Instructions.txt b/30_Cube/csharp/Resources/Instructions.txt new file mode 100644 index 00000000..e82ae51e --- /dev/null +++ b/30_Cube/csharp/Resources/Instructions.txt @@ -0,0 +1,24 @@ +This is a game in which you will be playing against the +random decision od the computer. The field of play is a +cube of side 3. Any of the 27 locations can be designated +by inputing three numbers such as 2,3,1. At the start, +you are automatically at location 1,1,1. The object of +the game is to get to location 3,3,3. One minor detail: +the computer will pick, at random, 5 locations at which +it will play land mines. If you hit one of these locations +you lose. One other details: you may move only one space +in one direction each move. For example: from 1,1,2 you +may move to 2,1,2 or 1,1,3. You may not change +two of the numbers on the same move. If you make an illegal +move, you lose and the computer takes the money you may +have bet on that round. + + +All Yes or No questions will be answered by a 1 for Yes +or a 0 (zero) for no. + +When stating the amount of a wager, print only the number +of dollars (example: 250) You are automatically started with +500 dollars in your account. + +Good luck! diff --git a/30_Cube/csharp/Resources/Introduction.txt b/30_Cube/csharp/Resources/Introduction.txt new file mode 100644 index 00000000..6299d19b --- /dev/null +++ b/30_Cube/csharp/Resources/Introduction.txt @@ -0,0 +1,6 @@ + Cube + Creative Computing Morristown, New Jersey + + + +Do you want to see the instructions? (Yes--1,No--0) diff --git a/30_Cube/csharp/Resources/NextMove.txt b/30_Cube/csharp/Resources/NextMove.txt new file mode 100644 index 00000000..4cbe5496 --- /dev/null +++ b/30_Cube/csharp/Resources/NextMove.txt @@ -0,0 +1 @@ +Next move: \ No newline at end of file diff --git a/30_Cube/csharp/Resources/Resource.cs b/30_Cube/csharp/Resources/Resource.cs new file mode 100644 index 00000000..c3ed10c7 --- /dev/null +++ b/30_Cube/csharp/Resources/Resource.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Cube.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Introduction => GetStream(); + public static Stream Instructions => GetStream(); + public static Stream Wager => GetStream(); + public static Stream IllegalMove => GetStream(); + public static Stream Bang => GetStream(); + public static Stream Bust => GetStream(); + public static Stream Congratulations => GetStream(); + public static Stream Goodbye => GetStream(); + } + + internal static class Prompts + { + public static string HowMuch => GetString(); + public static string BetAgain => GetString(); + public static string YourMove => GetString(); + public static string NextMove => GetString(); + public static string TryAgain => GetString(); + } + + internal static class Formats + { + public static string Balance => GetString(); + } + + private static string GetString([CallerMemberName] string? name = null) + { + using var stream = GetStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/30_Cube/csharp/Resources/TryAgain.txt b/30_Cube/csharp/Resources/TryAgain.txt new file mode 100644 index 00000000..9ccf358a --- /dev/null +++ b/30_Cube/csharp/Resources/TryAgain.txt @@ -0,0 +1 @@ +Do you want to try again \ No newline at end of file diff --git a/30_Cube/csharp/Resources/Wager.txt b/30_Cube/csharp/Resources/Wager.txt new file mode 100644 index 00000000..04720a7a --- /dev/null +++ b/30_Cube/csharp/Resources/Wager.txt @@ -0,0 +1 @@ +Want to make a wager diff --git a/30_Cube/csharp/Resources/YourMove.txt b/30_Cube/csharp/Resources/YourMove.txt new file mode 100644 index 00000000..5ea0c544 --- /dev/null +++ b/30_Cube/csharp/Resources/YourMove.txt @@ -0,0 +1,2 @@ + +It's your move: \ No newline at end of file diff --git a/30_Cube/csharp/ZerosGenerator.cs b/30_Cube/csharp/ZerosGenerator.cs new file mode 100644 index 00000000..4490f606 --- /dev/null +++ b/30_Cube/csharp/ZerosGenerator.cs @@ -0,0 +1,10 @@ +namespace Cube; + +internal class ZerosGenerator : IRandom +{ + public float NextFloat() => 0; + + public float PreviousFloat() => 0; + + public void Reseed(int seed) { } +} \ No newline at end of file