Merge pull request #781 from drewjcooper/csharp-30-cube

C# 30 cube
This commit is contained in:
Jeff Atwood
2022-08-25 14:39:41 -07:00
committed by GitHub
23 changed files with 292 additions and 0 deletions

View File

@@ -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.

View File

@@ -6,4 +6,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

104
30_Cube/csharp/Game.cs Normal file
View File

@@ -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
};
}

View File

@@ -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;
}
}
}

10
30_Cube/csharp/Program.cs Normal file
View File

@@ -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();

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -0,0 +1 @@
You now have {0} dollars.

View File

@@ -0,0 +1,4 @@
******BANG******
You lose!

View File

@@ -0,0 +1 @@
Tried to fool me; bet again

View File

@@ -0,0 +1 @@
You bust.

View File

@@ -0,0 +1 @@
Congratulations!

View File

@@ -0,0 +1,3 @@
Tough luck!
Goodbye.

View File

@@ -0,0 +1 @@
How much

View File

@@ -0,0 +1,2 @@
Illegal move. You lose.

View File

@@ -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!

View File

@@ -0,0 +1,6 @@
Cube
Creative Computing Morristown, New Jersey
Do you want to see the instructions? (Yes--1,No--0)

View File

@@ -0,0 +1 @@
Next move:

View File

@@ -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}'.");
}

View File

@@ -0,0 +1 @@
Do you want to try again

View File

@@ -0,0 +1 @@
Want to make a wager

View File

@@ -0,0 +1,2 @@
It's your move:

View File

@@ -0,0 +1,10 @@
namespace Cube;
internal class ZerosGenerator : IRandom
{
public float NextFloat() => 0;
public float PreviousFloat() => 0;
public void Reseed(int seed) { }
}