diff --git a/13_Bounce/csharp/Bounce.cs b/13_Bounce/csharp/Bounce.cs new file mode 100644 index 00000000..c250e7df --- /dev/null +++ b/13_Bounce/csharp/Bounce.cs @@ -0,0 +1,38 @@ +namespace Bounce; + +/// +/// Represents the bounce of the ball, calculating duration, height and position in time. +/// +/// +/// All calculations are derived from the equation for projectile motion: s = vt + 0.5at^2 +/// +internal class Bounce +{ + private const float _acceleration = -32; // feet/s^2 + + private readonly float _velocity; + + internal Bounce(float velocity) + { + _velocity = velocity; + } + + public float Duration => -2 * _velocity / _acceleration; + + public float MaxHeight => + (float)Math.Round(-_velocity * _velocity / 2 / _acceleration, MidpointRounding.AwayFromZero); + + public float Plot(Graph graph, float startTime) + { + var time = 0f; + for (; time <= Duration; time += graph.TimeIncrement) + { + var height = _velocity * time + _acceleration * time * time / 2; + graph.Plot(startTime + time, height); + } + + return startTime + time; + } + + public Bounce Next(float elasticity) => new Bounce(_velocity * elasticity); +} diff --git a/13_Bounce/csharp/Bounce.csproj b/13_Bounce/csharp/Bounce.csproj index d3fe4757..e377b904 100644 --- a/13_Bounce/csharp/Bounce.csproj +++ b/13_Bounce/csharp/Bounce.csproj @@ -6,4 +6,12 @@ enable enable + + + + + + + + diff --git a/13_Bounce/csharp/Game.cs b/13_Bounce/csharp/Game.cs new file mode 100644 index 00000000..155bdf48 --- /dev/null +++ b/13_Bounce/csharp/Game.cs @@ -0,0 +1,38 @@ +using static Bounce.Resources.Resource; + +namespace Bounce; + +internal class Game +{ + private readonly IReadWrite _io; + + public Game(IReadWrite io) + { + _io = io; + } + + public void Play(Func playAgain) + { + _io.Write(Streams.Title); + _io.Write(Streams.Instructions); + + while (playAgain.Invoke()) + { + var timeIncrement = _io.ReadParameter("Time increment (sec)"); + var velocity = _io.ReadParameter("Velocity (fps)"); + var elasticity = _io.ReadParameter("Coefficient"); + + var bounce = new Bounce(velocity); + var bounceCount = (int)(Graph.Row.Width * timeIncrement / bounce.Duration); + var graph = new Graph(bounce.MaxHeight, timeIncrement); + + var time = 0f; + for (var i = 0; i < bounceCount; i++, bounce = bounce.Next(elasticity)) + { + time = bounce.Plot(graph, time); + } + + _io.WriteLine(graph); + } + } +} diff --git a/13_Bounce/csharp/Graph.cs b/13_Bounce/csharp/Graph.cs new file mode 100644 index 00000000..608b5bda --- /dev/null +++ b/13_Bounce/csharp/Graph.cs @@ -0,0 +1,116 @@ +using System.Text; + +namespace Bounce; + +/// +/// Provides support for plotting a graph of height vs time, and rendering it to a string. +/// +internal class Graph +{ + private readonly Dictionary _rows; + + public Graph(float maxHeight, float timeIncrement) + { + // 1 row == 1/2 foot + 1 row for zero + var rowCount = 2 * (int)Math.Round(maxHeight, MidpointRounding.AwayFromZero) + 1; + _rows = Enumerable.Range(0, rowCount) + .ToDictionary(x => x, x => new Row(x % 2 == 0 ? $" {x / 2} " : "")); + TimeIncrement = timeIncrement; + } + + public float TimeIncrement { get; } + public float MaxTimePlotted { get; private set; } + + public void Plot(float time, float height) + { + var rowIndex = (int)Math.Round(height * 2, MidpointRounding.AwayFromZero); + var colIndex = (int)(time / TimeIncrement) + 1; + if (_rows.TryGetValue(rowIndex, out var row)) + { + row[colIndex] = '0'; + } + MaxTimePlotted = Math.Max(time, MaxTimePlotted); + } + + public override string ToString() + { + var sb = new StringBuilder().AppendLine("Feet").AppendLine(); + foreach (var (_, row) in _rows.OrderByDescending(x => x.Key)) + { + sb.Append(row).AppendLine(); + } + sb.Append(new Axis(MaxTimePlotted, TimeIncrement)); + + return sb.ToString(); + } + + internal class Row + { + public const int Width = 70; + + private readonly char[] _chars = new char[Width + 2]; + private int nextColumn = 0; + + public Row(string label) + { + Array.Fill(_chars, ' '); + Array.Copy(label.ToCharArray(), _chars, label.Length); + nextColumn = label.Length; + } + + public char this[int column] + { + set + { + if (column >= _chars.Length) { return; } + if (column < nextColumn) { column = nextColumn; } + _chars[column] = value; + nextColumn = column + 1; + } + } + + public override string ToString() => new string(_chars); + } + + internal class Axis + { + private readonly int _maxTimeMark; + private readonly float _timeIncrement; + private readonly Labels _labels; + + internal Axis(float maxTimePlotted, float timeIncrement) + { + _maxTimeMark = (int)Math.Ceiling(maxTimePlotted); + _timeIncrement = timeIncrement; + + _labels = new Labels(); + for (var i = 1; i <= _maxTimeMark; i++) + { + _labels.Add((int)(i / _timeIncrement), $" {i} "); + } + } + + public override string ToString() + => new StringBuilder() + .Append(' ').Append('.', (int)(_maxTimeMark / _timeIncrement) + 1).AppendLine() + .Append(_labels).AppendLine() + .Append(' ', (int)(_maxTimeMark / _timeIncrement / 2 - 2)).AppendLine("Seconds") + .ToString(); + } + + internal class Labels : Row + { + public Labels() + : base(" 0") + { + } + + public void Add(int column, string label) + { + for (var i = 0; i < label.Length; i++) + { + this[column + i] = label[i]; + } + } + } +} diff --git a/13_Bounce/csharp/IReadWriteExtensions.cs b/13_Bounce/csharp/IReadWriteExtensions.cs new file mode 100644 index 00000000..bafeee42 --- /dev/null +++ b/13_Bounce/csharp/IReadWriteExtensions.cs @@ -0,0 +1,11 @@ +namespace Bounce; + +internal static class IReadWriteExtensions +{ + internal static float ReadParameter(this IReadWrite io, string parameter) + { + var value = io.ReadNumber(parameter); + io.WriteLine(); + return value; + } +} \ No newline at end of file diff --git a/13_Bounce/csharp/Program.cs b/13_Bounce/csharp/Program.cs new file mode 100644 index 00000000..86af3365 --- /dev/null +++ b/13_Bounce/csharp/Program.cs @@ -0,0 +1,6 @@ +global using Games.Common.IO; +global using Games.Common.Numbers; + +using Bounce; + +new Game(new ConsoleIO()).Play(() => true); \ No newline at end of file diff --git a/13_Bounce/csharp/README.md b/13_Bounce/csharp/README.md index 4daabb5c..6323d9d1 100644 --- a/13_Bounce/csharp/README.md +++ b/13_Bounce/csharp/README.md @@ -1,3 +1,26 @@ +# Bounce + 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/) + +## Conversion notes + +### Mode of Operation + +This conversion performs the same function as the original, and provides the same experience, but does it in a different +way. + +The original BASIC code builds the graph as it writes to the screen, scanning each line for points that need to be +plotted. + +This conversion steps through time, calculating the position of the ball at each instant, building the graph in memory. +It then writes the graph to the output in one go. + +### Failure Modes + +The original BASIC code performs no validation of the input parameters. Some combinations of parameters produce no +output, others crash the program. + +In the spirit of the original this conversion also performs no validation of the parameters, but it does not attempt to +replicate the original's failure modes. It fails quite happily in its own way. diff --git a/13_Bounce/csharp/Resources/Instructions.txt b/13_Bounce/csharp/Resources/Instructions.txt new file mode 100644 index 00000000..d2901d6e --- /dev/null +++ b/13_Bounce/csharp/Resources/Instructions.txt @@ -0,0 +1,8 @@ +This simulation lets you specify the initial velocity +of a ball thrown straight up, and the coefficient of +elasticity of the ball. Please use a decimal fraction +coefficiency (less than 1). + +You also specify the time increment to be used in +'strobing' the ball's flight (try .1 initially). + diff --git a/13_Bounce/csharp/Resources/Resource.cs b/13_Bounce/csharp/Resources/Resource.cs new file mode 100644 index 00000000..68bebee0 --- /dev/null +++ b/13_Bounce/csharp/Resources/Resource.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Bounce.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Instructions => GetStream(); + public static Stream Title => GetStream(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) + => Assembly.GetExecutingAssembly().GetManifestResourceStream($"Bounce.Resources.{name}.txt") + ?? throw new ArgumentException($"Resource stream {name} does not exist", nameof(name)); +} \ No newline at end of file diff --git a/13_Bounce/csharp/Resources/Title.txt b/13_Bounce/csharp/Resources/Title.txt new file mode 100644 index 00000000..5bcd0496 --- /dev/null +++ b/13_Bounce/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Bounce + Creative Computing Morristown, New Jersey + + +