diff --git a/07_Basketball/README.md b/07_Basketball/README.md
index 5200748d..ac0662dd 100644
--- a/07_Basketball/README.md
+++ b/07_Basketball/README.md
@@ -14,7 +14,7 @@ Both teams use the same defense, but you may call it:
- Enter (7): Zone
- Enter (7.5): None
-To change defense, type “0” as your next shot.
+To change defense, type "0" as your next shot.
Note: The game is biased slightly in favor of Dartmouth. The average probability of a Dartmouth shot being good is 62.95% compared to a probability of 61.85% for their opponent. (This makes the sample run slightly remarkable in that Cornell won by a score of 45 to 42 Hooray for the Big Red!)
@@ -33,3 +33,19 @@ http://www.vintage-basic.net/games.html
(please note any difficulties or challenges in porting here)
+##### Original bugs
+
+###### Initial defense selection
+
+If a number <6 is entered for the starting defense then the original code prompts again until a value >=6 is entered,
+but then skips the opponent selection center jump.
+
+The C# port does not reproduce this behavior. It does prompt for a correct value, but will then go to opponent selection
+followed by the center jump.
+
+###### Unvalidated defense selection
+
+The original code does not validate the value entered for the defense beyond checking that it is >=6. A large enough
+defense value will guarantee that all shots are good, and the game gets rather predictable.
+
+This bug is preserved in the C# port.
diff --git a/07_Basketball/csharp/Basketball.csproj b/07_Basketball/csharp/Basketball.csproj
index d3fe4757..91e759c0 100644
--- a/07_Basketball/csharp/Basketball.csproj
+++ b/07_Basketball/csharp/Basketball.csproj
@@ -6,4 +6,13 @@
enable
enable
+
+
+
+
+
+
+
+
+
diff --git a/07_Basketball/csharp/Clock.cs b/07_Basketball/csharp/Clock.cs
new file mode 100644
index 00000000..25033052
--- /dev/null
+++ b/07_Basketball/csharp/Clock.cs
@@ -0,0 +1,25 @@
+using Basketball.Resources;
+using Games.Common.IO;
+
+namespace Basketball;
+
+internal class Clock
+{
+ private readonly IReadWrite _io;
+ private int time;
+
+ public Clock(IReadWrite io) => _io = io;
+
+ public bool IsHalfTime => time == 50;
+ public bool IsFullTime => time >= 100;
+ public bool TwoMinutesLeft => time == 92;
+
+ public void Increment(Scoreboard scoreboard)
+ {
+ time += 1;
+ if (IsHalfTime) { scoreboard.Display(Resource.Formats.EndOfFirstHalf); }
+ if (TwoMinutesLeft) { _io.Write(Resource.Streams.TwoMinutesLeft); }
+ }
+
+ public void StartOvertime() => time = 93;
+}
\ No newline at end of file
diff --git a/07_Basketball/csharp/Defense.cs b/07_Basketball/csharp/Defense.cs
new file mode 100644
index 00000000..f447266f
--- /dev/null
+++ b/07_Basketball/csharp/Defense.cs
@@ -0,0 +1,12 @@
+namespace Basketball;
+
+internal class Defense
+{
+ private float _value;
+
+ public Defense(float value) => Set(value);
+
+ public void Set(float value) => _value = value;
+
+ public static implicit operator float(Defense defense) => defense._value;
+}
diff --git a/07_Basketball/csharp/Game.cs b/07_Basketball/csharp/Game.cs
new file mode 100644
index 00000000..5a7d6ffa
--- /dev/null
+++ b/07_Basketball/csharp/Game.cs
@@ -0,0 +1,73 @@
+using Basketball.Plays;
+using Basketball.Resources;
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+namespace Basketball;
+
+internal class Game
+{
+ private readonly Clock _clock;
+ private readonly Scoreboard _scoreboard;
+ private readonly TextIO _io;
+ private readonly IRandom _random;
+
+ private Game(Clock clock, Scoreboard scoreboard, TextIO io, IRandom random)
+ {
+ _clock = clock;
+ _scoreboard = scoreboard;
+ _io = io;
+ _random = random;
+ }
+
+ public static Game Create(TextIO io, IRandom random)
+ {
+ io.Write(Resource.Streams.Introduction);
+
+ var defense = new Defense(io.ReadDefense("Your starting defense will be"));
+ var clock = new Clock(io);
+
+ io.WriteLine();
+
+ var scoreboard = new Scoreboard(
+ new Team("Dartmouth", new HomeTeamPlay(io, random, clock, defense)),
+ new Team(io.ReadString("Choose your opponent"), new VisitingTeamPlay(io, random, clock, defense)),
+ io);
+
+ return new Game(clock, scoreboard, io, random);
+ }
+
+ public void Play()
+ {
+ var ballContest = new BallContest(0.4f, "{0} controls the tap", _io, _random);
+
+ while (true)
+ {
+ _io.WriteLine("Center jump");
+ ballContest.Resolve(_scoreboard);
+
+ _io.WriteLine();
+
+ while (true)
+ {
+ var isFullTime = _scoreboard.Offense.ResolvePlay(_scoreboard);
+ if (isFullTime && IsGameOver()) { return; }
+ if (_clock.IsHalfTime) { break; }
+ }
+ }
+ }
+
+ private bool IsGameOver()
+ {
+ _io.WriteLine();
+ if (_scoreboard.ScoresAreEqual)
+ {
+ _scoreboard.Display(Resource.Formats.EndOfSecondHalf);
+ _clock.StartOvertime();
+ return false;
+ }
+
+ _scoreboard.Display(Resource.Formats.EndOfGame);
+ return true;
+ }
+}
diff --git a/07_Basketball/csharp/IRandomExtensions.cs b/07_Basketball/csharp/IRandomExtensions.cs
new file mode 100644
index 00000000..876e05a2
--- /dev/null
+++ b/07_Basketball/csharp/IRandomExtensions.cs
@@ -0,0 +1,8 @@
+using Games.Common.Randomness;
+
+namespace Basketball;
+
+internal static class IRandomExtensions
+{
+ internal static Shot NextShot(this IRandom random) => Shot.Get(random.NextFloat(1, 3.5f));
+}
diff --git a/07_Basketball/csharp/IReadWriteExtensions.cs b/07_Basketball/csharp/IReadWriteExtensions.cs
new file mode 100644
index 00000000..6197ff97
--- /dev/null
+++ b/07_Basketball/csharp/IReadWriteExtensions.cs
@@ -0,0 +1,34 @@
+using Games.Common.IO;
+
+namespace Basketball;
+
+internal static class IReadWriteExtensions
+{
+ public static float ReadDefense(this IReadWrite io, string prompt)
+ {
+ while (true)
+ {
+ var defense = io.ReadNumber(prompt);
+ if (defense >= 6) { return defense; }
+ }
+ }
+
+ private static bool TryReadInteger(this IReadWrite io, string prompt, out int intValue)
+ {
+ var floatValue = io.ReadNumber(prompt);
+ intValue = (int)floatValue;
+ return intValue == floatValue;
+ }
+
+ public static Shot? ReadShot(this IReadWrite io, string prompt)
+ {
+ while (true)
+ {
+ if (io.TryReadInteger(prompt, out var value) && Shot.TryGet(value, out var shot))
+ {
+ return shot;
+ }
+ io.Write("Incorrect answer. Retype it. ");
+ }
+ }
+}
diff --git a/07_Basketball/csharp/JumpShot.cs b/07_Basketball/csharp/JumpShot.cs
new file mode 100644
index 00000000..46ffc6d8
--- /dev/null
+++ b/07_Basketball/csharp/JumpShot.cs
@@ -0,0 +1,9 @@
+namespace Basketball;
+
+public class JumpShot : Shot
+{
+ public JumpShot()
+ : base("Jump shot")
+ {
+ }
+}
\ No newline at end of file
diff --git a/07_Basketball/csharp/Plays/BallContest.cs b/07_Basketball/csharp/Plays/BallContest.cs
new file mode 100644
index 00000000..a12ddc36
--- /dev/null
+++ b/07_Basketball/csharp/Plays/BallContest.cs
@@ -0,0 +1,28 @@
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+namespace Basketball.Plays;
+
+internal class BallContest
+{
+ private readonly float _probability;
+ private readonly string _messageFormat;
+ private readonly IReadWrite _io;
+ private readonly IRandom _random;
+
+ internal BallContest(float probability, string messageFormat, IReadWrite io, IRandom random)
+ {
+ _io = io;
+ _probability = probability;
+ _messageFormat = messageFormat;
+ _random = random;
+ }
+
+ internal bool Resolve(Scoreboard scoreboard)
+ {
+ var winner = _random.NextFloat() <= _probability ? scoreboard.Home : scoreboard.Visitors;
+ scoreboard.Offense = winner;
+ _io.WriteLine(_messageFormat, winner);
+ return false;
+ }
+}
diff --git a/07_Basketball/csharp/Plays/HomeTeamPlay.cs b/07_Basketball/csharp/Plays/HomeTeamPlay.cs
new file mode 100644
index 00000000..48ef249e
--- /dev/null
+++ b/07_Basketball/csharp/Plays/HomeTeamPlay.cs
@@ -0,0 +1,93 @@
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+namespace Basketball.Plays;
+
+internal class HomeTeamPlay : Play
+{
+ private readonly TextIO _io;
+ private readonly IRandom _random;
+ private readonly Clock _clock;
+ private readonly Defense _defense;
+ private readonly BallContest _ballContest;
+
+ public HomeTeamPlay(TextIO io, IRandom random, Clock clock, Defense defense)
+ : base(io, random, clock)
+ {
+ _io = io;
+ _random = random;
+ _clock = clock;
+ _defense = defense;
+ _ballContest = new BallContest(0.5f, "Shot is blocked. Ball controlled by {0}.", _io, _random);
+ }
+
+ internal override bool Resolve(Scoreboard scoreboard)
+ {
+ var shot = _io.ReadShot("Your shot");
+
+ if (_random.NextFloat() >= 0.5f && _clock.IsFullTime) { return true; }
+
+ if (shot is null)
+ {
+ _defense.Set(_io.ReadDefense("Your new defensive alignment is"));
+ _io.WriteLine();
+ return false;
+ }
+
+ if (shot is JumpShot jumpShot)
+ {
+ if (ClockIncrementsToHalfTime(scoreboard)) { return false; }
+ if (!Resolve(jumpShot, scoreboard)) { return false; }
+ }
+
+ do
+ {
+ if (ClockIncrementsToHalfTime(scoreboard)) { return false; }
+ } while (Resolve(shot, scoreboard));
+
+ return false;
+ }
+
+ // The Resolve* methods resolve the probabilistic outcome of the current game state.
+ // They return true if the Home team should continue the play and attempt a layup, false otherwise.
+ private bool Resolve(JumpShot shot, Scoreboard scoreboard) =>
+ Resolve(shot.ToString(), _defense / 8)
+ .Do(0.341f, () => scoreboard.AddBasket("Shot is good"))
+ .Or(0.682f, () => ResolveShotOffTarget(scoreboard))
+ .Or(0.782f, () => _ballContest.Resolve(scoreboard))
+ .Or(0.843f, () => ResolveFreeThrows(scoreboard, "Shooter is fouled. Two shots."))
+ .Or(() => scoreboard.Turnover($"Charging foul. {scoreboard.Home} loses ball."));
+
+ private bool Resolve(Shot shot, Scoreboard scoreboard) =>
+ Resolve(shot.ToString(), _defense / 7)
+ .Do(0.4f, () => scoreboard.AddBasket("Shot is good. Two points."))
+ .Or(0.7f, () => ResolveShotOffTheRim(scoreboard))
+ .Or(0.875f, () => ResolveFreeThrows(scoreboard, "Shooter fouled. Two shots."))
+ .Or(0.925f, () => scoreboard.Turnover($"Shot blocked. {scoreboard.Visitors}'s ball."))
+ .Or(() => scoreboard.Turnover($"Charging foul. {scoreboard.Home} loses ball."));
+
+ private bool ResolveShotOffTarget(Scoreboard scoreboard) =>
+ Resolve("Shot is off target", 6 / _defense)
+ .Do(0.45f, () => ResolveHomeRebound(scoreboard, ResolvePossibleSteal))
+ .Or(() => scoreboard.Turnover($"Rebound to {scoreboard.Visitors}"));
+
+ private bool ResolveHomeRebound(Scoreboard scoreboard, Action endOfPlayAction) =>
+ Resolve($"{scoreboard.Home} controls the rebound.")
+ .Do(0.4f, () => true)
+ .Or(() => endOfPlayAction.Invoke(scoreboard));
+ private void ResolvePossibleSteal(Scoreboard scoreboard)
+ {
+ if (_defense == 6 && _random.NextFloat() > 0.6f)
+ {
+ scoreboard.Turnover();
+ scoreboard.AddBasket($"Pass stolen by {scoreboard.Visitors} easy layup.");
+ _io.WriteLine();
+ }
+ _io.Write("Ball passed back to you. ");
+ }
+
+ private void ResolveShotOffTheRim(Scoreboard scoreboard) =>
+ Resolve("Shot is off the rim.")
+ .Do(2 / 3f, () => scoreboard.Turnover($"{scoreboard.Visitors} controls the rebound."))
+ .Or(() => ResolveHomeRebound(scoreboard, _ => _io.WriteLine("Ball passed back to you.")));
+}
diff --git a/07_Basketball/csharp/Plays/Play.cs b/07_Basketball/csharp/Plays/Play.cs
new file mode 100644
index 00000000..9b82a74e
--- /dev/null
+++ b/07_Basketball/csharp/Plays/Play.cs
@@ -0,0 +1,40 @@
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+namespace Basketball.Plays;
+
+internal abstract class Play
+{
+ private readonly IReadWrite _io;
+ private readonly IRandom _random;
+ private readonly Clock _clock;
+
+ public Play(IReadWrite io, IRandom random, Clock clock)
+ {
+ _io = io;
+ _random = random;
+ _clock = clock;
+ }
+
+ protected bool ClockIncrementsToHalfTime(Scoreboard scoreboard)
+ {
+ _clock.Increment(scoreboard);
+ return _clock.IsHalfTime;
+ }
+
+ internal abstract bool Resolve(Scoreboard scoreboard);
+
+ protected void ResolveFreeThrows(Scoreboard scoreboard, string message) =>
+ Resolve(message)
+ .Do(0.49f, () => scoreboard.AddFreeThrows(2, "Shooter makes both shots."))
+ .Or(0.75f, () => scoreboard.AddFreeThrows(1, "Shooter makes one shot and misses one."))
+ .Or(() => scoreboard.AddFreeThrows(0, "Both shots missed."));
+
+ protected Probably Resolve(string message) => Resolve(message, 1f);
+
+ protected Probably Resolve(string message, float defenseFactor)
+ {
+ _io.WriteLine(message);
+ return new Probably(defenseFactor, _random);
+ }
+}
diff --git a/07_Basketball/csharp/Plays/VisitingTeamPlay.cs b/07_Basketball/csharp/Plays/VisitingTeamPlay.cs
new file mode 100644
index 00000000..3975a6ba
--- /dev/null
+++ b/07_Basketball/csharp/Plays/VisitingTeamPlay.cs
@@ -0,0 +1,81 @@
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+namespace Basketball.Plays;
+
+internal class VisitingTeamPlay : Play
+{
+ private readonly TextIO _io;
+ private readonly IRandom _random;
+ private readonly Defense _defense;
+
+ public VisitingTeamPlay(TextIO io, IRandom random, Clock clock, Defense defense)
+ : base(io, random, clock)
+ {
+ _io = io;
+ _random = random;
+ _defense = defense;
+ }
+
+ internal override bool Resolve(Scoreboard scoreboard)
+ {
+ if (ClockIncrementsToHalfTime(scoreboard)) { return false; }
+
+ _io.WriteLine();
+ var shot = _random.NextShot();
+
+ if (shot is JumpShot jumpShot)
+ {
+ var continuePlay = Resolve(jumpShot, scoreboard);
+ _io.WriteLine();
+ if (!continuePlay) { return false; }
+ }
+
+ while (true)
+ {
+ var continuePlay = Resolve(shot, scoreboard);
+ _io.WriteLine();
+ if (!continuePlay) { return false; }
+ }
+ }
+
+ // The Resolve* methods resolve the probabilistic outcome of the current game state.
+ // They return true if the Visiting team should continue the play and attempt a layup, false otherwise.
+ private bool Resolve(JumpShot shot, Scoreboard scoreboard) =>
+ Resolve(shot.ToString(), _defense / 8)
+ .Do(0.35f, () => scoreboard.AddBasket("Shot is good."))
+ .Or(0.75f, () => ResolveBadShot(scoreboard, "Shot is off the rim.", _defense * 6))
+ .Or(0.9f, () => ResolveFreeThrows(scoreboard, "Player fouled. Two shots."))
+ .Or(() => _io.WriteLine($"Offensive foul. {scoreboard.Home}'s ball."));
+
+ private bool Resolve(Shot shot, Scoreboard scoreboard) =>
+ Resolve(shot.ToString(), _defense / 7)
+ .Do(0.413f, () => scoreboard.AddBasket("Shot is good."))
+ .Or(() => ResolveBadShot(scoreboard, "Shot is missed.", 6 / _defense));
+
+ private bool ResolveBadShot(Scoreboard scoreboard, string message, float defenseFactor) =>
+ Resolve(message, defenseFactor)
+ .Do(0.5f, () => scoreboard.Turnover($"{scoreboard.Home} controls the rebound."))
+ .Or(() => ResolveVisitorsRebound(scoreboard));
+
+ private bool ResolveVisitorsRebound(Scoreboard scoreboard)
+ {
+ _io.Write($"{scoreboard.Visitors} controls the rebound.");
+ if (_defense == 6 && _random.NextFloat() <= 0.25f)
+ {
+ _io.WriteLine();
+ scoreboard.Turnover();
+ scoreboard.AddBasket($"Ball stolen. Easy lay up for {scoreboard.Home}.");
+ return false;
+ }
+
+ if (_random.NextFloat() <= 0.5f)
+ {
+ _io.WriteLine();
+ _io.Write($"Pass back to {scoreboard.Visitors} guard.");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/07_Basketball/csharp/Probably.cs b/07_Basketball/csharp/Probably.cs
new file mode 100644
index 00000000..0ba5864a
--- /dev/null
+++ b/07_Basketball/csharp/Probably.cs
@@ -0,0 +1,50 @@
+using Games.Common.Randomness;
+
+namespace Basketball;
+
+///
+/// Supports a chain of actions to be performed based on various probabilities. The original game code gets a new
+/// random number for each probability check. Evaluating a set of probabilities against a single random number is
+/// much simpler, but yield a very different outcome distribution. The purpose of this class is to simplify the code
+/// to for the original probabilistic branch decisions.
+///
+internal struct Probably
+{
+ private readonly float _defenseFactor;
+ private readonly IRandom _random;
+ private readonly bool? _result;
+
+ internal Probably(float defenseFactor, IRandom random, bool? result = null)
+ {
+ _defenseFactor = defenseFactor;
+ _random = random;
+ _result = result;
+ }
+
+ public Probably Do(float probability, Action action) =>
+ ShouldResolveAction(probability)
+ ? new Probably(_defenseFactor, _random, Resolve(action) ?? false)
+ : this;
+
+ public Probably Do(float probability, Func action) =>
+ ShouldResolveAction(probability)
+ ? new Probably(_defenseFactor, _random, Resolve(action) ?? false)
+ : this;
+
+ public Probably Or(float probability, Action action) => Do(probability, action);
+
+ public Probably Or(float probability, Func action) => Do(probability, action);
+
+ public bool Or(Action action) => _result ?? Resolve(action) ?? false;
+
+ private bool? Resolve(Action action)
+ {
+ action.Invoke();
+ return _result;
+ }
+
+ private bool? Resolve(Func action) => action.Invoke();
+
+ private readonly bool ShouldResolveAction(float probability) =>
+ _result is null && _random.NextFloat() <= probability * _defenseFactor;
+}
diff --git a/07_Basketball/csharp/Program.cs b/07_Basketball/csharp/Program.cs
new file mode 100644
index 00000000..7a610336
--- /dev/null
+++ b/07_Basketball/csharp/Program.cs
@@ -0,0 +1,7 @@
+using Basketball;
+using Games.Common.IO;
+using Games.Common.Randomness;
+
+var game = Game.Create(new ConsoleIO(), new RandomNumberGenerator());
+
+game.Play();
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/EndOfFirstHalf.txt b/07_Basketball/csharp/Resources/EndOfFirstHalf.txt
new file mode 100644
index 00000000..6a132a0e
--- /dev/null
+++ b/07_Basketball/csharp/Resources/EndOfFirstHalf.txt
@@ -0,0 +1,5 @@
+
+ ***** End of first half *****
+
+Score: {0}: {1} {2}: {3}
+
diff --git a/07_Basketball/csharp/Resources/EndOfGame.txt b/07_Basketball/csharp/Resources/EndOfGame.txt
new file mode 100644
index 00000000..fc65416c
--- /dev/null
+++ b/07_Basketball/csharp/Resources/EndOfGame.txt
@@ -0,0 +1,2 @@
+ ***** End of game *****
+Final score: {0}: {1} {2}: {3}
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/EndOfSecondHalf.txt b/07_Basketball/csharp/Resources/EndOfSecondHalf.txt
new file mode 100644
index 00000000..c5f73218
--- /dev/null
+++ b/07_Basketball/csharp/Resources/EndOfSecondHalf.txt
@@ -0,0 +1,7 @@
+
+ ***** End of second half *****
+
+Score at end of regulation time:
+ {0}: {1} {2}: {3}
+
+Begin two minute overtime period
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/Introduction.txt b/07_Basketball/csharp/Resources/Introduction.txt
new file mode 100644
index 00000000..b5ad510b
--- /dev/null
+++ b/07_Basketball/csharp/Resources/Introduction.txt
@@ -0,0 +1,12 @@
+ Basketball
+ Creative Computing Morristown, New Jersey
+
+
+
+This is Dartmouth College basketball. You will be Dartmouth
+ captain and playmaker. Call shots as follows: 1. Long
+ (30 ft.) jump shot; 2. Short (15 ft.) jump shot; 3. Lay
+ up; 4. Set shot.
+Both teams will use the same defense. Call defense as
+follows: 6. Press; 6.5 Man-to-man; 7. Zone; 7.5 None.
+To change defense, just type 0 as your next shot.
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/Resource.cs b/07_Basketball/csharp/Resources/Resource.cs
new file mode 100644
index 00000000..c442a3ff
--- /dev/null
+++ b/07_Basketball/csharp/Resources/Resource.cs
@@ -0,0 +1,32 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Basketball.Resources;
+
+internal static class Resource
+{
+ internal static class Streams
+ {
+ public static Stream Introduction => GetStream();
+ public static Stream TwoMinutesLeft => GetStream();
+ }
+
+ internal static class Formats
+ {
+ public static string EndOfFirstHalf => GetString();
+ public static string EndOfGame => GetString();
+ public static string EndOfSecondHalf => GetString();
+ public static string Score => 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($"Basketball.Resources.{name}.txt")
+ ?? throw new Exception($"Could not find embedded resource stream '{name}'.");
+}
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/Score.txt b/07_Basketball/csharp/Resources/Score.txt
new file mode 100644
index 00000000..7b317cb3
--- /dev/null
+++ b/07_Basketball/csharp/Resources/Score.txt
@@ -0,0 +1 @@
+Score: {1} to {3}
\ No newline at end of file
diff --git a/07_Basketball/csharp/Resources/TwoMinutesLeft.txt b/07_Basketball/csharp/Resources/TwoMinutesLeft.txt
new file mode 100644
index 00000000..0ad16993
--- /dev/null
+++ b/07_Basketball/csharp/Resources/TwoMinutesLeft.txt
@@ -0,0 +1,3 @@
+
+ *** Two minutes left in the game ***
+
diff --git a/07_Basketball/csharp/Scoreboard.cs b/07_Basketball/csharp/Scoreboard.cs
new file mode 100644
index 00000000..79068392
--- /dev/null
+++ b/07_Basketball/csharp/Scoreboard.cs
@@ -0,0 +1,48 @@
+using Basketball.Resources;
+using Games.Common.IO;
+
+namespace Basketball;
+
+internal class Scoreboard
+{
+ private readonly Dictionary _scores;
+ private readonly IReadWrite _io;
+
+ public Scoreboard(Team home, Team visitors, IReadWrite io)
+ {
+ _scores = new() { [home] = 0, [visitors] = 0 };
+ Home = home;
+ Visitors = visitors;
+ Offense = home; // temporary value till first center jump
+ _io = io;
+ }
+
+ public bool ScoresAreEqual => _scores[Home] == _scores[Visitors];
+ public Team Offense { get; set; }
+ public Team Home { get; }
+ public Team Visitors { get; }
+
+ public void AddBasket(string message) => AddScore(2, message);
+
+ public void AddFreeThrows(uint count, string message) => AddScore(count, message);
+
+ private void AddScore(uint score, string message)
+ {
+ if (Offense is null) { throw new InvalidOperationException("Offense must be set before adding to score."); }
+
+ _io.WriteLine(message);
+ _scores[Offense] += score;
+ Turnover();
+ Display();
+ }
+
+ public void Turnover(string? message = null)
+ {
+ if (message is not null) { _io.WriteLine(message); }
+
+ Offense = Offense == Home ? Visitors : Home;
+ }
+
+ public void Display(string? format = null) =>
+ _io.WriteLine(format ?? Resource.Formats.Score, Home, _scores[Home], Visitors, _scores[Visitors]);
+}
diff --git a/07_Basketball/csharp/Shot.cs b/07_Basketball/csharp/Shot.cs
new file mode 100644
index 00000000..fb7c2f3e
--- /dev/null
+++ b/07_Basketball/csharp/Shot.cs
@@ -0,0 +1,37 @@
+namespace Basketball;
+
+public class Shot
+{
+ private readonly string _name;
+
+ public Shot(string name)
+ {
+ _name = name;
+ }
+
+ public static bool TryGet(int shotNumber, out Shot? shot)
+ {
+ shot = shotNumber switch
+ {
+ // Although the game instructions reference two different jump shots,
+ // the original game code treats them both the same and just prints "Jump shot"
+ 0 => null,
+ <= 2 => new JumpShot(),
+ 3 => new Shot("Lay up"),
+ 4 => new Shot("Set shot"),
+ _ => null
+ };
+ return shotNumber == 0 || shot is not null;
+ }
+
+ public static Shot Get(float shotNumber) =>
+ shotNumber switch
+ {
+ <= 2 => new JumpShot(),
+ > 3 => new Shot("Set shot"),
+ > 2 => new Shot("Lay up"),
+ _ => throw new Exception("Unexpected value")
+ };
+
+ public override string ToString() => _name;
+}
diff --git a/07_Basketball/csharp/Team.cs b/07_Basketball/csharp/Team.cs
new file mode 100644
index 00000000..2cf262aa
--- /dev/null
+++ b/07_Basketball/csharp/Team.cs
@@ -0,0 +1,10 @@
+using Basketball.Plays;
+
+namespace Basketball;
+
+internal record Team(string Name, Play PlayResolver)
+{
+ public override string ToString() => Name;
+
+ public bool ResolvePlay(Scoreboard scoreboard) => PlayResolver.Resolve(scoreboard);
+}