diff --git a/12_Bombs_Away/csharp/BombsAway.sln b/12_Bombs_Away/csharp/BombsAway.sln
new file mode 100644
index 00000000..8ae70157
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAway.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BombsAwayConsole", "BombsAwayConsole\BombsAwayConsole.csproj", "{D80015FA-423C-4A16-AA2B-16669245AD59}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BombsAwayGame", "BombsAwayGame\BombsAwayGame.csproj", "{F57AEC18-FEE9-4F08-9F20-DFC56EFE6BFC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D80015FA-423C-4A16-AA2B-16669245AD59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D80015FA-423C-4A16-AA2B-16669245AD59}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D80015FA-423C-4A16-AA2B-16669245AD59}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D80015FA-423C-4A16-AA2B-16669245AD59}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F57AEC18-FEE9-4F08-9F20-DFC56EFE6BFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F57AEC18-FEE9-4F08-9F20-DFC56EFE6BFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F57AEC18-FEE9-4F08-9F20-DFC56EFE6BFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F57AEC18-FEE9-4F08-9F20-DFC56EFE6BFC}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {39B2ECFB-037D-4335-BBD2-64892E953DD4}
+ EndGlobalSection
+EndGlobal
diff --git a/12_Bombs_Away/csharp/BombsAwayConsole/BombsAwayConsole.csproj b/12_Bombs_Away/csharp/BombsAwayConsole/BombsAwayConsole.csproj
new file mode 100644
index 00000000..aae99def
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayConsole/BombsAwayConsole.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/12_Bombs_Away/csharp/BombsAwayConsole/ConsoleUserInterface.cs b/12_Bombs_Away/csharp/BombsAwayConsole/ConsoleUserInterface.cs
new file mode 100644
index 00000000..66d6d4d3
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayConsole/ConsoleUserInterface.cs
@@ -0,0 +1,134 @@
+namespace BombsAwayConsole;
+
+///
+/// Implements by writing to and reading from .
+///
+internal class ConsoleUserInterface : BombsAwayGame.IUserInterface
+{
+ ///
+ /// Write message to console.
+ ///
+ /// Message to display.
+ public void Output(string message)
+ {
+ Console.WriteLine(message);
+ }
+
+ ///
+ /// Write choices with affixed indexes, allowing the user to choose by index.
+ ///
+ /// Message to display.
+ /// Choices to display.
+ /// Choice that user picked.
+ public int Choose(string message, IList choices)
+ {
+ IEnumerable choicesWithIndexes = choices.Select((choice, index) => $"{choice}({index + 1})");
+ string choiceText = string.Join(", ", choicesWithIndexes);
+ Output($"{message} -- {choiceText}");
+
+ ISet allowedKeys = ConsoleKeysFromList(choices);
+ ConsoleKey? choice;
+ do
+ {
+ choice = ReadChoice(allowedKeys);
+ if (choice is null)
+ {
+ Output("TRY AGAIN...");
+ }
+ }
+ while (choice is null);
+
+ return ListIndexFromConsoleKey(choice.Value);
+ }
+
+ ///
+ /// Convert the given list to its equivalents. This generates keys that map
+ /// the first element to , the second element to ,
+ /// and so on, up to the last element of the list.
+ ///
+ /// List whose elements will be converted to equivalents.
+ /// equivalents from .
+ private ISet ConsoleKeysFromList(IList list)
+ {
+ IEnumerable indexes = Enumerable.Range((int)ConsoleKey.D1, list.Count);
+ return new HashSet(indexes.Cast());
+ }
+
+ ///
+ /// Convert the given console key to its list index equivalent. This assumes the key was generated from
+ ///
+ ///
+ /// Key to convert to its list index equivalent.
+ /// List index equivalent of key.
+ private int ListIndexFromConsoleKey(ConsoleKey key)
+ {
+ return key - ConsoleKey.D1;
+ }
+
+ ///
+ /// Read a key from the console and return it if it is in the given allowed keys.
+ ///
+ /// Allowed keys.
+ /// Key read from , if it is in ; null otherwise./>
+ private ConsoleKey? ReadChoice(ISet allowedKeys)
+ {
+ ConsoleKeyInfo keyInfo = ReadKey();
+ return allowedKeys.Contains(keyInfo.Key) ? keyInfo.Key : null;
+ }
+
+ ///
+ /// Read key from .
+ ///
+ /// Key read from .
+ private ConsoleKeyInfo ReadKey()
+ {
+ ConsoleKeyInfo result = Console.ReadKey(intercept: false);
+ // Write a blank line to the console so the displayed key is on its own line.
+ Console.WriteLine();
+ return result;
+ }
+
+ ///
+ /// Allow user to choose 'Y' or 'N' from .
+ ///
+ /// Message to display.
+ /// True if user chose 'Y', false if user chose 'N'.
+ public bool ChooseYesOrNo(string message)
+ {
+ Output(message);
+ ConsoleKey? choice;
+ do
+ {
+ choice = ReadChoice(new HashSet(new[] { ConsoleKey.Y, ConsoleKey.N }));
+ if (choice is null)
+ {
+ Output("ENTER Y OR N");
+ }
+ }
+ while (choice is null);
+
+ return choice.Value == ConsoleKey.Y;
+ }
+
+ ///
+ /// Get integer by reading a line from .
+ ///
+ /// Integer read from .
+ public int InputInteger()
+ {
+ bool resultIsValid;
+ int result;
+ do
+ {
+ string? integerText = Console.ReadLine();
+ resultIsValid = int.TryParse(integerText, out result);
+ if (!resultIsValid)
+ {
+ Output("PLEASE ENTER A NUMBER");
+ }
+ }
+ while (!resultIsValid);
+
+ return result;
+ }
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayConsole/Program.cs b/12_Bombs_Away/csharp/BombsAwayConsole/Program.cs
new file mode 100644
index 00000000..35728cfc
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayConsole/Program.cs
@@ -0,0 +1,26 @@
+using BombsAwayConsole;
+using BombsAwayGame;
+
+/// Create and play s using a .
+PlayGameWhileUserWantsTo(new ConsoleUserInterface());
+
+void PlayGameWhileUserWantsTo(ConsoleUserInterface ui)
+{
+ do
+ {
+ new Game(ui).Play();
+ }
+ while (UserWantsToPlayAgain(ui));
+}
+
+bool UserWantsToPlayAgain(IUserInterface ui)
+{
+ bool result = ui.ChooseYesOrNo("ANOTHER MISSION (Y OR N)?");
+ if (!result)
+ {
+ Console.WriteLine("CHICKEN !!!");
+ }
+
+ return result;
+}
+
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/AlliesSide.cs b/12_Bombs_Away/csharp/BombsAwayGame/AlliesSide.cs
new file mode 100644
index 00000000..c6c7105b
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/AlliesSide.cs
@@ -0,0 +1,22 @@
+namespace BombsAwayGame;
+
+///
+/// Allies protagonist. Can fly missions in a Liberator, B-29, B-17, or Lancaster.
+///
+internal class AlliesSide : MissionSide
+{
+ public AlliesSide(IUserInterface ui)
+ : base(ui)
+ {
+ }
+
+ protected override string ChooseMissionMessage => "AIRCRAFT";
+
+ protected override IList AllMissions => new Mission[]
+ {
+ new("LIBERATOR", "YOU'VE GOT 2 TONS OF BOMBS FLYING FOR PLOESTI."),
+ new("B-29", "YOU'RE DUMPING THE A-BOMB ON HIROSHIMA."),
+ new("B-17", "YOU'RE CHASING THE BISMARK IN THE NORTH SEA."),
+ new("LANCASTER", "YOU'RE BUSTING A GERMAN HEAVY WATER PLANT IN THE RUHR.")
+ };
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/BombsAwayGame.csproj b/12_Bombs_Away/csharp/BombsAwayGame/BombsAwayGame.csproj
new file mode 100644
index 00000000..132c02c5
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/BombsAwayGame.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/EnemyArtillery.cs b/12_Bombs_Away/csharp/BombsAwayGame/EnemyArtillery.cs
new file mode 100644
index 00000000..a810c8c0
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/EnemyArtillery.cs
@@ -0,0 +1,8 @@
+namespace BombsAwayGame;
+
+///
+/// Represents enemy artillery.
+///
+/// Name of artillery type.
+/// Accuracy of artillery. This is the `T` variable in the original BASIC.
+internal record class EnemyArtillery(string Name, int Accuracy);
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/Game.cs b/12_Bombs_Away/csharp/BombsAwayGame/Game.cs
new file mode 100644
index 00000000..d6b5c3e9
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/Game.cs
@@ -0,0 +1,58 @@
+namespace BombsAwayGame;
+
+///
+/// Plays the Bombs Away game using a supplied .
+///
+public class Game
+{
+ private readonly IUserInterface _ui;
+
+ ///
+ /// Create game instance using the given UI.
+ ///
+ /// UI to use for game.
+ public Game(IUserInterface ui)
+ {
+ _ui = ui;
+ }
+
+ ///
+ /// Play game. Choose a side and play the side's logic.
+ ///
+ public void Play()
+ {
+ _ui.Output("YOU ARE A PILOT IN A WORLD WAR II BOMBER.");
+ Side side = ChooseSide();
+ side.Play();
+ }
+
+ ///
+ /// Represents a .
+ ///
+ /// Name of side.
+ /// Create instance of side that this descriptor represents.
+ private record class SideDescriptor(string Name, Func CreateSide);
+
+ ///
+ /// Choose side and return a new instance of that side.
+ ///
+ /// New instance of side that was chosen.
+ private Side ChooseSide()
+ {
+ SideDescriptor[] sides = AllSideDescriptors;
+ string[] sideNames = sides.Select(a => a.Name).ToArray();
+ int index = _ui.Choose("WHAT SIDE", sideNames);
+ return sides[index].CreateSide();
+ }
+
+ ///
+ /// All side descriptors.
+ ///
+ private SideDescriptor[] AllSideDescriptors => new SideDescriptor[]
+ {
+ new("ITALY", () => new ItalySide(_ui)),
+ new("ALLIES", () => new AlliesSide(_ui)),
+ new("JAPAN", () => new JapanSide(_ui)),
+ new("GERMANY", () => new GermanySide(_ui)),
+ };
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/GermanySide.cs b/12_Bombs_Away/csharp/BombsAwayGame/GermanySide.cs
new file mode 100644
index 00000000..99843fce
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/GermanySide.cs
@@ -0,0 +1,21 @@
+namespace BombsAwayGame;
+
+///
+/// Germany protagonist. Can fly missions to Russia, England, and France.
+///
+internal class GermanySide : MissionSide
+{
+ public GermanySide(IUserInterface ui)
+ : base(ui)
+ {
+ }
+
+ protected override string ChooseMissionMessage => "A NAZI, EH? OH WELL. ARE YOU GOING FOR";
+
+ protected override IList AllMissions => new Mission[]
+ {
+ new("RUSSIA", "YOU'RE NEARING STALINGRAD."),
+ new("ENGLAND", "NEARING LONDON. BE CAREFUL, THEY'VE GOT RADAR."),
+ new("FRANCE", "NEARING VERSAILLES. DUCK SOUP. THEY'RE NEARLY DEFENSELESS.")
+ };
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/IUserInterface.cs b/12_Bombs_Away/csharp/BombsAwayGame/IUserInterface.cs
new file mode 100644
index 00000000..50b7828c
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/IUserInterface.cs
@@ -0,0 +1,38 @@
+namespace BombsAwayGame;
+
+///
+/// Represents an interface for supplying data to the game.
+///
+///
+/// Abstracting the UI allows us to concentrate its concerns in one part of our code and to change UI behavior
+/// without creating any risk of changing the game logic. It also allows us to supply an automated UI for tests.
+///
+public interface IUserInterface
+{
+ ///
+ /// Display the given message.
+ ///
+ /// Message to display.
+ void Output(string message);
+
+ ///
+ /// Choose an item from the given choices.
+ ///
+ /// Message to display.
+ /// Choices to choose from.
+ /// Index of choice in that user chose.
+ int Choose(string message, IList choices);
+
+ ///
+ /// Allow user to choose Yes or No.
+ ///
+ /// Message to display.
+ /// True if user chose Yes, false if user chose No.
+ bool ChooseYesOrNo(string message);
+
+ ///
+ /// Get integer from user.
+ ///
+ /// Integer supplied by user.
+ int InputInteger();
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/ItalySide.cs b/12_Bombs_Away/csharp/BombsAwayGame/ItalySide.cs
new file mode 100644
index 00000000..9f8bcd83
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/ItalySide.cs
@@ -0,0 +1,21 @@
+namespace BombsAwayGame;
+
+///
+/// Italy protagonist. Can fly missions to Albania, Greece, and North Africa.
+///
+internal class ItalySide : MissionSide
+{
+ public ItalySide(IUserInterface ui)
+ : base(ui)
+ {
+ }
+
+ protected override string ChooseMissionMessage => "YOUR TARGET";
+
+ protected override IList AllMissions => new Mission[]
+ {
+ new("ALBANIA", "SHOULD BE EASY -- YOU'RE FLYING A NAZI-MADE PLANE."),
+ new("GREECE", "BE CAREFUL!!!"),
+ new("NORTH AFRICA", "YOU'RE GOING FOR THE OIL, EH?")
+ };
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/JapanSide.cs b/12_Bombs_Away/csharp/BombsAwayGame/JapanSide.cs
new file mode 100644
index 00000000..33abc83b
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/JapanSide.cs
@@ -0,0 +1,38 @@
+namespace BombsAwayGame;
+
+///
+/// Japan protagonist. Flies a kamikaze mission, which has a different logic from s.
+///
+internal class JapanSide : Side
+{
+ public JapanSide(IUserInterface ui)
+ : base(ui)
+ {
+ }
+
+ ///
+ /// Perform a kamikaze mission. If first kamikaze mission, it will succeed 65% of the time. If it's not
+ /// first kamikaze mission, perform an enemy counterattack.
+ ///
+ public override void Play()
+ {
+ UI.Output("YOU'RE FLYING A KAMIKAZE MISSION OVER THE USS LEXINGTON.");
+
+ bool isFirstMission = UI.ChooseYesOrNo("YOUR FIRST KAMIKAZE MISSION(Y OR N)?");
+ if (!isFirstMission)
+ {
+ // LINE 207 of original BASIC: hitRatePercent is initialized to 0,
+ // but R, the type of artillery, is not initialized at all. Setting
+ // R = 1, which is to say EnemyArtillery = Guns, gives the same result.
+ EnemyCounterattack(Guns, hitRatePercent: 0);
+ }
+ else if (RandomFrac() > 0.65)
+ {
+ MissionSucceeded();
+ }
+ else
+ {
+ MissionFailed();
+ }
+ }
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/Mission.cs b/12_Bombs_Away/csharp/BombsAwayGame/Mission.cs
new file mode 100644
index 00000000..c93892fd
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/Mission.cs
@@ -0,0 +1,8 @@
+namespace BombsAwayGame;
+
+///
+/// Represents a mission that can be flown by a .
+///
+/// Name of mission.
+/// Description of mission.
+internal record class Mission(string Name, string Description);
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/MissionSide.cs b/12_Bombs_Away/csharp/BombsAwayGame/MissionSide.cs
new file mode 100644
index 00000000..b3a54275
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/MissionSide.cs
@@ -0,0 +1,208 @@
+namespace BombsAwayGame;
+
+///
+/// Represents a protagonist that chooses a standard (non-kamikaze) mission.
+///
+internal abstract class MissionSide : Side
+{
+ ///
+ /// Create instance using the given UI.
+ ///
+ /// UI to use.
+ public MissionSide(IUserInterface ui)
+ : base(ui)
+ {
+ }
+
+ ///
+ /// Reasonable upper bound for missions flown previously.
+ ///
+ private const int MaxMissionCount = 160;
+
+ ///
+ /// Choose a mission and attempt it. If attempt fails, perform an enemy counterattack.
+ ///
+ public override void Play()
+ {
+ Mission mission = ChooseMission();
+ UI.Output(mission.Description);
+
+ int missionCount = MissionCountFromUI();
+ CommentOnMissionCount(missionCount);
+
+ AttemptMission(missionCount);
+ }
+
+ ///
+ /// Choose a mission.
+ ///
+ /// Mission chosen.
+ private Mission ChooseMission()
+ {
+ IList missions = AllMissions;
+ string[] missionNames = missions.Select(a => a.Name).ToArray();
+ int index = UI.Choose(ChooseMissionMessage, missionNames);
+ return missions[index];
+ }
+
+ ///
+ /// Message to display when choosing a mission.
+ ///
+ protected abstract string ChooseMissionMessage { get; }
+
+ ///
+ /// All aviailable missions to choose from.
+ ///
+ protected abstract IList AllMissions { get; }
+
+ ///
+ /// Get mission count from UI. If mission count exceeds a reasonable maximum, ask UI again.
+ ///
+ /// Mission count from UI.
+ private int MissionCountFromUI()
+ {
+ const string HowManyMissions = "HOW MANY MISSIONS HAVE YOU FLOWN?";
+ string inputMessage = HowManyMissions;
+
+ bool resultIsValid;
+ int result;
+ do
+ {
+ UI.Output(inputMessage);
+ result = UI.InputInteger();
+ if (result < 0)
+ {
+ UI.Output($"NUMBER OF MISSIONS CAN'T BE NEGATIVE.");
+ resultIsValid = false;
+ }
+ else if (result > MaxMissionCount)
+ {
+ resultIsValid = false;
+ UI.Output($"MISSIONS, NOT MILES...{MaxMissionCount} MISSIONS IS HIGH EVEN FOR OLD-TIMERS.");
+ inputMessage = "NOW THEN, " + HowManyMissions;
+ }
+ else
+ {
+ resultIsValid = true;
+ }
+ }
+ while (!resultIsValid);
+
+ return result;
+ }
+
+ ///
+ /// Display a message about the given mission count, if it is unusually high or low.
+ ///
+ /// Mission count to comment on.
+ private void CommentOnMissionCount(int missionCount)
+ {
+ if (missionCount >= 100)
+ {
+ UI.Output("THAT'S PUSHING THE ODDS!");
+ }
+ else if (missionCount < 25)
+ {
+ UI.Output("FRESH OUT OF TRAINING, EH?");
+ }
+ }
+
+ ///
+ /// Attempt mission.
+ ///
+ /// Number of missions previously flown. Higher mission counts will yield a higher probability of success.
+ private void AttemptMission(int missionCount)
+ {
+ if (missionCount < RandomInteger(0, MaxMissionCount))
+ {
+ MissedTarget();
+ }
+ else
+ {
+ MissionSucceeded();
+ }
+ }
+
+ ///
+ /// Display message indicating that target was missed. Choose enemy artillery and perform a counterattack.
+ ///
+ private void MissedTarget()
+ {
+ UI.Output("MISSED TARGET BY " + (2 + RandomInteger(0, 30)) + " MILES!");
+ UI.Output("NOW YOU'RE REALLY IN FOR IT !!");
+
+ // Choose enemy and counterattack.
+ EnemyArtillery enemyArtillery = ChooseEnemyArtillery();
+
+ if (enemyArtillery == Missiles)
+ {
+ EnemyCounterattack(enemyArtillery, hitRatePercent: 0);
+ }
+ else
+ {
+ int hitRatePercent = EnemyHitRatePercentFromUI();
+ if (hitRatePercent < MinEnemyHitRatePercent)
+ {
+ UI.Output("YOU LIE, BUT YOU'LL PAY...");
+ MissionFailed();
+ }
+ else
+ {
+ EnemyCounterattack(enemyArtillery, hitRatePercent);
+ }
+ }
+ }
+
+ ///
+ /// Choose enemy artillery from UI.
+ ///
+ /// Artillery chosen.
+ private EnemyArtillery ChooseEnemyArtillery()
+ {
+ EnemyArtillery[] artilleries = new EnemyArtillery[] { Guns, Missiles, Both };
+ string[] artilleryNames = artilleries.Select(a => a.Name).ToArray();
+ int index = UI.Choose("DOES THE ENEMY HAVE", artilleryNames);
+ return artilleries[index];
+ }
+
+ ///
+ /// Minimum allowed hit rate percent.
+ ///
+ private const int MinEnemyHitRatePercent = 10;
+
+ ///
+ /// Maximum allowed hit rate percent.
+ ///
+ private const int MaxEnemyHitRatePercent = 50;
+
+ ///
+ /// Get the enemy hit rate percent from UI. Value must be between zero and .
+ /// If value is less than , mission fails automatically because the user is
+ /// assumed to be untruthful.
+ ///
+ /// Enemy hit rate percent from UI.
+ private int EnemyHitRatePercentFromUI()
+ {
+ UI.Output($"WHAT'S THE PERCENT HIT RATE OF ENEMY GUNNERS ({MinEnemyHitRatePercent} TO {MaxEnemyHitRatePercent})");
+
+ bool resultIsValid;
+ int result;
+ do
+ {
+ result = UI.InputInteger();
+ // Let them enter a number below the stated minimum, as they will be caught and punished.
+ if (0 <= result && result <= MaxEnemyHitRatePercent)
+ {
+ resultIsValid = true;
+ }
+ else
+ {
+ resultIsValid = false;
+ UI.Output($"NUMBER MUST BE FROM {MinEnemyHitRatePercent} TO {MaxEnemyHitRatePercent}");
+ }
+ }
+ while (!resultIsValid);
+
+ return result;
+ }
+}
diff --git a/12_Bombs_Away/csharp/BombsAwayGame/Side.cs b/12_Bombs_Away/csharp/BombsAwayGame/Side.cs
new file mode 100644
index 00000000..7e643971
--- /dev/null
+++ b/12_Bombs_Away/csharp/BombsAwayGame/Side.cs
@@ -0,0 +1,97 @@
+namespace BombsAwayGame;
+
+///
+/// Represents a protagonist in the game.
+///
+internal abstract class Side
+{
+ ///
+ /// Create instance using the given UI.
+ ///
+ /// UI to use.
+ public Side(IUserInterface ui)
+ {
+ UI = ui;
+ }
+
+ ///
+ /// Play this side.
+ ///
+ public abstract void Play();
+
+ ///
+ /// User interface supplied to ctor.
+ ///
+ protected IUserInterface UI { get; }
+
+ ///
+ /// Random-number generator for this play-through.
+ ///
+ private readonly Random _random = new();
+
+ ///
+ /// Gets a random floating-point number greater than or equal to zero, and less than one.
+ ///
+ /// Random floating-point number greater than or equal to zero, and less than one.
+ protected double RandomFrac() => _random.NextDouble();
+
+ ///
+ /// Gets a random integer in a range.
+ ///
+ /// The inclusive lower bound of the number returned.
+ /// The exclusive upper bound of the number returned.
+ /// Random integer in a range.
+ protected int RandomInteger(int minValue, int maxValue) => _random.Next(minValue: minValue, maxValue: maxValue);
+
+ ///
+ /// Display messages indicating the mission succeeded.
+ ///
+ protected void MissionSucceeded()
+ {
+ UI.Output("DIRECT HIT!!!! " + RandomInteger(0, 100) + " KILLED.");
+ UI.Output("MISSION SUCCESSFUL.");
+ }
+
+ ///
+ /// Gets the Guns type of enemy artillery.
+ ///
+ protected EnemyArtillery Guns { get; } = new("GUNS", 0);
+
+ ///
+ /// Gets the Missiles type of enemy artillery.
+ ///
+ protected EnemyArtillery Missiles { get; } = new("MISSILES", 35);
+
+ ///
+ /// Gets the Both Guns and Missiles type of enemy artillery.
+ ///
+ protected EnemyArtillery Both { get; } = new("BOTH", 35);
+
+ ///
+ /// Perform enemy counterattack using the given artillery and hit rate percent.
+ ///
+ /// Enemy artillery to use.
+ /// Hit rate percent for enemy.
+ protected void EnemyCounterattack(EnemyArtillery artillery, int hitRatePercent)
+ {
+ if (hitRatePercent + artillery.Accuracy > RandomInteger(0, 100))
+ {
+ MissionFailed();
+ }
+ else
+ {
+ UI.Output("YOU MADE IT THROUGH TREMENDOUS FLAK!!");
+ }
+ }
+
+ ///
+ /// Display messages indicating the mission failed.
+ ///
+ protected void MissionFailed()
+ {
+ UI.Output("* * * * BOOM * * * *");
+ UI.Output("YOU HAVE BEEN SHOT DOWN.....");
+ UI.Output("DEARLY BELOVED, WE ARE GATHERED HERE TODAY TO PAY OUR");
+ UI.Output("LAST TRIBUTE...");
+ }
+}