From c4c67e15a23ff0a281795422d41b07dec0effae5 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sat, 16 Apr 2022 22:34:08 +1000 Subject: [PATCH 1/5] Add resources --- 13_Bounce/csharp/Bounce.csproj | 8 ++++++++ 13_Bounce/csharp/Resources/Instructions.txt | 8 ++++++++ 13_Bounce/csharp/Resources/Resource.cs | 17 +++++++++++++++++ 13_Bounce/csharp/Resources/Title.txt | 5 +++++ 4 files changed, 38 insertions(+) create mode 100644 13_Bounce/csharp/Resources/Instructions.txt create mode 100644 13_Bounce/csharp/Resources/Resource.cs create mode 100644 13_Bounce/csharp/Resources/Title.txt 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/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 + + + From e2599bc38bdf1c74704a281d61eb0273f198eb16 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sat, 16 Apr 2022 22:34:35 +1000 Subject: [PATCH 2/5] Intiial implementation --- 13_Bounce/csharp/Game.cs | 149 ++++++++++++++++++++++++++++++++++++ 13_Bounce/csharp/Program.cs | 6 ++ 2 files changed, 155 insertions(+) create mode 100644 13_Bounce/csharp/Game.cs create mode 100644 13_Bounce/csharp/Program.cs diff --git a/13_Bounce/csharp/Game.cs b/13_Bounce/csharp/Game.cs new file mode 100644 index 00000000..1bf8236d --- /dev/null +++ b/13_Bounce/csharp/Game.cs @@ -0,0 +1,149 @@ +using System.Text; +using static Bounce.Resources.Resource; + +namespace Bounce; + +internal class Game +{ + private const int _chartWidth = 70; + private const float _acceleration = -32; // feet/s^2 + + private readonly IReadWrite _io; + + public Game(IReadWrite io) + { + _io = io; + } + + public void Play() + { + _io.Write(Streams.Title); + _io.Write(Streams.Instructions); + + var increment = _io.ReadParameter("Time increment (sec)"); + var velocity = _io.ReadParameter("Velocity (fps)"); + var elasticity = _io.ReadParameter("Coefficient"); + + var timeToFirstBounce = -2 * velocity / _acceleration; + var bounceCount = (int)(Graph.Row.Width * increment / timeToFirstBounce); + + var maxHeight = (float)Math.Round(-velocity * velocity / 2 / _acceleration, MidpointRounding.AwayFromZero); + + var graph = new Graph(maxHeight, increment); + + var totalTime = 0f; + for (var bounce = 0; bounce < bounceCount; bounce++, velocity *= elasticity) + { + var bounceDuration = -2 * velocity / _acceleration; + for (var time = 0f; time <= bounceDuration; totalTime += increment, time += increment) + { + var height = velocity * time + _acceleration * time * time / 2; + graph.Plot(totalTime, height); + } + } + + _io.WriteLine(graph); + } +} + +internal class Graph +{ + private readonly Dictionary _rows; + private readonly float _increment; + private float _maxTimePlotted; + + public Graph(float maxHeight, float increment) + { + // 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} " : "")); + _increment = increment; + } + + public void Plot(float time, float height) + { + var rowIndex = (int)Math.Round(height * 2, MidpointRounding.AwayFromZero); + var colIndex = (int)(time / _increment) + 1; + if (_rows.TryGetValue(rowIndex, out var row)) + { + row[colIndex] = '0'; + } + _maxTimePlotted = Math.Max(time, _maxTimePlotted); + } + + public override string ToString() + { + var sb = new StringBuilder(); + foreach (var (_, row) in _rows.OrderByDescending(x => x.Key)) + { + sb.Append(row).AppendLine(); + } + var maxTimeMark = (int)Math.Ceiling(_maxTimePlotted); + sb.Append(' ').Append('.', (int)(maxTimeMark/_increment) + 1).AppendLine(); + + var timeLabels = new Labels(); + for (var i = 1; i <= maxTimeMark; i++) + { + timeLabels.Add((int)(i / _increment), $" {i} "); + } + sb.Append(timeLabels).AppendLine(); + sb.Append(' ', (int)((int)(_maxTimePlotted + 1)/_increment/2 - 2)).AppendLine("Seconds"); + + 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 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]; + } + } + } +} + +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..3b79a341 --- /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(); \ No newline at end of file From c9cd477022954c0558441ef2d2256914793eb2f5 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sun, 17 Apr 2022 19:33:02 +1000 Subject: [PATCH 3/5] Encapsulate calculations and graph logic --- 13_Bounce/csharp/Bounce.cs | 38 +++++++ 13_Bounce/csharp/Game.cs | 128 ++--------------------- 13_Bounce/csharp/Graph.cs | 116 ++++++++++++++++++++ 13_Bounce/csharp/IReadWriteExtensions.cs | 11 ++ 4 files changed, 172 insertions(+), 121 deletions(-) create mode 100644 13_Bounce/csharp/Bounce.cs create mode 100644 13_Bounce/csharp/Graph.cs create mode 100644 13_Bounce/csharp/IReadWriteExtensions.cs 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/Game.cs b/13_Bounce/csharp/Game.cs index 1bf8236d..8d2baf8b 100644 --- a/13_Bounce/csharp/Game.cs +++ b/13_Bounce/csharp/Game.cs @@ -1,13 +1,9 @@ -using System.Text; using static Bounce.Resources.Resource; namespace Bounce; internal class Game { - private const int _chartWidth = 70; - private const float _acceleration = -32; // feet/s^2 - private readonly IReadWrite _io; public Game(IReadWrite io) @@ -20,130 +16,20 @@ internal class Game _io.Write(Streams.Title); _io.Write(Streams.Instructions); - var increment = _io.ReadParameter("Time increment (sec)"); + var timeIncrement = _io.ReadParameter("Time increment (sec)"); var velocity = _io.ReadParameter("Velocity (fps)"); var elasticity = _io.ReadParameter("Coefficient"); - var timeToFirstBounce = -2 * velocity / _acceleration; - var bounceCount = (int)(Graph.Row.Width * increment / timeToFirstBounce); + var bounce = new Bounce(velocity); + var bounceCount = (int)(Graph.Row.Width * timeIncrement / bounce.Duration); + var graph = new Graph(bounce.MaxHeight, timeIncrement); - var maxHeight = (float)Math.Round(-velocity * velocity / 2 / _acceleration, MidpointRounding.AwayFromZero); - - var graph = new Graph(maxHeight, increment); - - var totalTime = 0f; - for (var bounce = 0; bounce < bounceCount; bounce++, velocity *= elasticity) + var time = 0f; + for (var i = 0; i < bounceCount; i++, bounce = bounce.Next(elasticity)) { - var bounceDuration = -2 * velocity / _acceleration; - for (var time = 0f; time <= bounceDuration; totalTime += increment, time += increment) - { - var height = velocity * time + _acceleration * time * time / 2; - graph.Plot(totalTime, height); - } + time = bounce.Plot(graph, time); } _io.WriteLine(graph); } } - -internal class Graph -{ - private readonly Dictionary _rows; - private readonly float _increment; - private float _maxTimePlotted; - - public Graph(float maxHeight, float increment) - { - // 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} " : "")); - _increment = increment; - } - - public void Plot(float time, float height) - { - var rowIndex = (int)Math.Round(height * 2, MidpointRounding.AwayFromZero); - var colIndex = (int)(time / _increment) + 1; - if (_rows.TryGetValue(rowIndex, out var row)) - { - row[colIndex] = '0'; - } - _maxTimePlotted = Math.Max(time, _maxTimePlotted); - } - - public override string ToString() - { - var sb = new StringBuilder(); - foreach (var (_, row) in _rows.OrderByDescending(x => x.Key)) - { - sb.Append(row).AppendLine(); - } - var maxTimeMark = (int)Math.Ceiling(_maxTimePlotted); - sb.Append(' ').Append('.', (int)(maxTimeMark/_increment) + 1).AppendLine(); - - var timeLabels = new Labels(); - for (var i = 1; i <= maxTimeMark; i++) - { - timeLabels.Add((int)(i / _increment), $" {i} "); - } - sb.Append(timeLabels).AppendLine(); - sb.Append(' ', (int)((int)(_maxTimePlotted + 1)/_increment/2 - 2)).AppendLine("Seconds"); - - 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 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]; - } - } - } -} - -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/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 From ab48a16f7f5ac6e8435985e8f52b2464598f3ca3 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sun, 17 Apr 2022 19:47:22 +1000 Subject: [PATCH 4/5] Add some comments and game loop --- 13_Bounce/csharp/Game.cs | 31 +++++++++++++++++-------------- 13_Bounce/csharp/Program.cs | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/13_Bounce/csharp/Game.cs b/13_Bounce/csharp/Game.cs index 8d2baf8b..155bdf48 100644 --- a/13_Bounce/csharp/Game.cs +++ b/13_Bounce/csharp/Game.cs @@ -11,25 +11,28 @@ internal class Game _io = io; } - public void Play() + public void Play(Func playAgain) { _io.Write(Streams.Title); _io.Write(Streams.Instructions); - 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)) + while (playAgain.Invoke()) { - time = bounce.Plot(graph, time); - } + var timeIncrement = _io.ReadParameter("Time increment (sec)"); + var velocity = _io.ReadParameter("Velocity (fps)"); + var elasticity = _io.ReadParameter("Coefficient"); - _io.WriteLine(graph); + 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/Program.cs b/13_Bounce/csharp/Program.cs index 3b79a341..86af3365 100644 --- a/13_Bounce/csharp/Program.cs +++ b/13_Bounce/csharp/Program.cs @@ -3,4 +3,4 @@ global using Games.Common.Numbers; using Bounce; -new Game(new ConsoleIO()).Play(); \ No newline at end of file +new Game(new ConsoleIO()).Play(() => true); \ No newline at end of file From 89dba2e3f83ff4e23a2c57e90464da90d493cc00 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sun, 17 Apr 2022 19:47:43 +1000 Subject: [PATCH 5/5] Add conversion notes to Readme --- 13_Bounce/csharp/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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.