Merge pull request #724 from drewjcooper/csharp-13-bounce

C# 13 bounce
This commit is contained in:
Jeff Atwood
2022-04-18 11:40:12 -07:00
committed by GitHub
10 changed files with 270 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
namespace Bounce;
/// <summary>
/// Represents the bounce of the ball, calculating duration, height and position in time.
/// </summary>
/// <remarks>
/// All calculations are derived from the equation for projectile motion: s = vt + 0.5at^2
/// </remarks>
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);
}

View File

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

38
13_Bounce/csharp/Game.cs Normal file
View File

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

116
13_Bounce/csharp/Graph.cs Normal file
View File

@@ -0,0 +1,116 @@
using System.Text;
namespace Bounce;
/// <summary>
/// Provides support for plotting a graph of height vs time, and rendering it to a string.
/// </summary>
internal class Graph
{
private readonly Dictionary<int, Row> _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];
}
}
}
}

View File

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

View File

@@ -0,0 +1,6 @@
global using Games.Common.IO;
global using Games.Common.Numbers;
using Bounce;
new Game(new ConsoleIO()).Play(() => true);

View File

@@ -1,3 +1,26 @@
# Bounce
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) 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 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.

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
Bounce
Creative Computing Morristown, New Jersey