mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2026-02-04 19:12:07 -08:00
Added Bombs Away in C#. This is a Visual Studio 2022 solution and uses the latest language features like file-scoped namespaces.
This commit is contained in:
31
12_Bombs_Away/csharp/BombsAway.sln
Normal file
31
12_Bombs_Away/csharp/BombsAway.sln
Normal file
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BombsAwayGame\BombsAwayGame.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
134
12_Bombs_Away/csharp/BombsAwayConsole/ConsoleUserInterface.cs
Normal file
134
12_Bombs_Away/csharp/BombsAwayConsole/ConsoleUserInterface.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
namespace BombsAwayConsole;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="BombsAwayGame.IUserInterface"/> by writing to and reading from <see cref="Console"/>.
|
||||
/// </summary>
|
||||
internal class ConsoleUserInterface : BombsAwayGame.IUserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Write message to console.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
public void Output(string message)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write choices with affixed indexes, allowing the user to choose by index.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
/// <param name="choices">Choices to display.</param>
|
||||
/// <returns>Choice that user picked.</returns>
|
||||
public int Choose(string message, IList<string> choices)
|
||||
{
|
||||
IEnumerable<string> choicesWithIndexes = choices.Select((choice, index) => $"{choice}({index + 1})");
|
||||
string choiceText = string.Join(", ", choicesWithIndexes);
|
||||
Output($"{message} -- {choiceText}");
|
||||
|
||||
ISet<ConsoleKey> allowedKeys = ConsoleKeysFromList(choices);
|
||||
ConsoleKey? choice;
|
||||
do
|
||||
{
|
||||
choice = ReadChoice(allowedKeys);
|
||||
if (choice is null)
|
||||
{
|
||||
Output("TRY AGAIN...");
|
||||
}
|
||||
}
|
||||
while (choice is null);
|
||||
|
||||
return ListIndexFromConsoleKey(choice.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert the given list to its <see cref="ConsoleKey"/> equivalents. This generates keys that map
|
||||
/// the first element to <see cref="ConsoleKey.D1"/>, the second element to <see cref="ConsoleKey.D2"/>,
|
||||
/// and so on, up to the last element of the list.
|
||||
/// </summary>
|
||||
/// <param name="list">List whose elements will be converted to <see cref="ConsoleKey"/> equivalents.</param>
|
||||
/// <returns><see cref="ConsoleKey"/> equivalents from <paramref name="list"/>.</returns>
|
||||
private ISet<ConsoleKey> ConsoleKeysFromList(IList<string> list)
|
||||
{
|
||||
IEnumerable<int> indexes = Enumerable.Range((int)ConsoleKey.D1, list.Count);
|
||||
return new HashSet<ConsoleKey>(indexes.Cast<ConsoleKey>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert the given console key to its list index equivalent. This assumes the key was generated from
|
||||
/// <see cref="ConsoleKeysFromList(IList{string})"/>
|
||||
/// </summary>
|
||||
/// <param name="key">Key to convert to its list index equivalent.</param>
|
||||
/// <returns>List index equivalent of key.</returns>
|
||||
private int ListIndexFromConsoleKey(ConsoleKey key)
|
||||
{
|
||||
return key - ConsoleKey.D1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a key from the console and return it if it is in the given allowed keys.
|
||||
/// </summary>
|
||||
/// <param name="allowedKeys">Allowed keys.</param>
|
||||
/// <returns>Key read from <see cref="Console"/>, if it is in <paramref name="allowedKeys"/>; null otherwise./></returns>
|
||||
private ConsoleKey? ReadChoice(ISet<ConsoleKey> allowedKeys)
|
||||
{
|
||||
ConsoleKeyInfo keyInfo = ReadKey();
|
||||
return allowedKeys.Contains(keyInfo.Key) ? keyInfo.Key : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read key from <see cref="Console"/>.
|
||||
/// </summary>
|
||||
/// <returns>Key read from <see cref="Console"/>.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow user to choose 'Y' or 'N' from <see cref="Console"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
/// <returns>True if user chose 'Y', false if user chose 'N'.</returns>
|
||||
public bool ChooseYesOrNo(string message)
|
||||
{
|
||||
Output(message);
|
||||
ConsoleKey? choice;
|
||||
do
|
||||
{
|
||||
choice = ReadChoice(new HashSet<ConsoleKey>(new[] { ConsoleKey.Y, ConsoleKey.N }));
|
||||
if (choice is null)
|
||||
{
|
||||
Output("ENTER Y OR N");
|
||||
}
|
||||
}
|
||||
while (choice is null);
|
||||
|
||||
return choice.Value == ConsoleKey.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get integer by reading a line from <see cref="Console"/>.
|
||||
/// </summary>
|
||||
/// <returns>Integer read from <see cref="Console"/>.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
26
12_Bombs_Away/csharp/BombsAwayConsole/Program.cs
Normal file
26
12_Bombs_Away/csharp/BombsAwayConsole/Program.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using BombsAwayConsole;
|
||||
using BombsAwayGame;
|
||||
|
||||
/// Create and play <see cref="Game"/>s using a <see cref="ConsoleUserInterface"/>.
|
||||
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;
|
||||
}
|
||||
|
||||
22
12_Bombs_Away/csharp/BombsAwayGame/AlliesSide.cs
Normal file
22
12_Bombs_Away/csharp/BombsAwayGame/AlliesSide.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Allies protagonist. Can fly missions in a Liberator, B-29, B-17, or Lancaster.
|
||||
/// </summary>
|
||||
internal class AlliesSide : MissionSide
|
||||
{
|
||||
public AlliesSide(IUserInterface ui)
|
||||
: base(ui)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string ChooseMissionMessage => "AIRCRAFT";
|
||||
|
||||
protected override IList<Mission> 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.")
|
||||
};
|
||||
}
|
||||
9
12_Bombs_Away/csharp/BombsAwayGame/BombsAwayGame.csproj
Normal file
9
12_Bombs_Away/csharp/BombsAwayGame/BombsAwayGame.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
8
12_Bombs_Away/csharp/BombsAwayGame/EnemyArtillery.cs
Normal file
8
12_Bombs_Away/csharp/BombsAwayGame/EnemyArtillery.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Represents enemy artillery.
|
||||
/// </summary>
|
||||
/// <param name="Name">Name of artillery type.</param>
|
||||
/// <param name="Accuracy">Accuracy of artillery. This is the `T` variable in the original BASIC.</param>
|
||||
internal record class EnemyArtillery(string Name, int Accuracy);
|
||||
58
12_Bombs_Away/csharp/BombsAwayGame/Game.cs
Normal file
58
12_Bombs_Away/csharp/BombsAwayGame/Game.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Plays the Bombs Away game using a supplied <see cref="IUserInterface"/>.
|
||||
/// </summary>
|
||||
public class Game
|
||||
{
|
||||
private readonly IUserInterface _ui;
|
||||
|
||||
/// <summary>
|
||||
/// Create game instance using the given UI.
|
||||
/// </summary>
|
||||
/// <param name="ui">UI to use for game.</param>
|
||||
public Game(IUserInterface ui)
|
||||
{
|
||||
_ui = ui;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play game. Choose a side and play the side's logic.
|
||||
/// </summary>
|
||||
public void Play()
|
||||
{
|
||||
_ui.Output("YOU ARE A PILOT IN A WORLD WAR II BOMBER.");
|
||||
Side side = ChooseSide();
|
||||
side.Play();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <see cref="Side"/>.
|
||||
/// </summary>
|
||||
/// <param name="Name">Name of side.</param>
|
||||
/// <param name="CreateSide">Create instance of side that this descriptor represents.</param>
|
||||
private record class SideDescriptor(string Name, Func<Side> CreateSide);
|
||||
|
||||
/// <summary>
|
||||
/// Choose side and return a new instance of that side.
|
||||
/// </summary>
|
||||
/// <returns>New instance of side that was chosen.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All side descriptors.
|
||||
/// </summary>
|
||||
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)),
|
||||
};
|
||||
}
|
||||
21
12_Bombs_Away/csharp/BombsAwayGame/GermanySide.cs
Normal file
21
12_Bombs_Away/csharp/BombsAwayGame/GermanySide.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Germany protagonist. Can fly missions to Russia, England, and France.
|
||||
/// </summary>
|
||||
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<Mission> 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.")
|
||||
};
|
||||
}
|
||||
38
12_Bombs_Away/csharp/BombsAwayGame/IUserInterface.cs
Normal file
38
12_Bombs_Away/csharp/BombsAwayGame/IUserInterface.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an interface for supplying data to the game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public interface IUserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Display the given message.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
void Output(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Choose an item from the given choices.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
/// <param name="choices">Choices to choose from.</param>
|
||||
/// <returns>Index of choice in <paramref name="choices"/> that user chose.</returns>
|
||||
int Choose(string message, IList<string> choices);
|
||||
|
||||
/// <summary>
|
||||
/// Allow user to choose Yes or No.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to display.</param>
|
||||
/// <returns>True if user chose Yes, false if user chose No.</returns>
|
||||
bool ChooseYesOrNo(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Get integer from user.
|
||||
/// </summary>
|
||||
/// <returns>Integer supplied by user.</returns>
|
||||
int InputInteger();
|
||||
}
|
||||
21
12_Bombs_Away/csharp/BombsAwayGame/ItalySide.cs
Normal file
21
12_Bombs_Away/csharp/BombsAwayGame/ItalySide.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Italy protagonist. Can fly missions to Albania, Greece, and North Africa.
|
||||
/// </summary>
|
||||
internal class ItalySide : MissionSide
|
||||
{
|
||||
public ItalySide(IUserInterface ui)
|
||||
: base(ui)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string ChooseMissionMessage => "YOUR TARGET";
|
||||
|
||||
protected override IList<Mission> 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?")
|
||||
};
|
||||
}
|
||||
38
12_Bombs_Away/csharp/BombsAwayGame/JapanSide.cs
Normal file
38
12_Bombs_Away/csharp/BombsAwayGame/JapanSide.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Japan protagonist. Flies a kamikaze mission, which has a different logic from <see cref="MissionSide"/>s.
|
||||
/// </summary>
|
||||
internal class JapanSide : Side
|
||||
{
|
||||
public JapanSide(IUserInterface ui)
|
||||
: base(ui)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
12_Bombs_Away/csharp/BombsAwayGame/Mission.cs
Normal file
8
12_Bombs_Away/csharp/BombsAwayGame/Mission.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mission that can be flown by a <see cref="MissionSide"/>.
|
||||
/// </summary>
|
||||
/// <param name="Name">Name of mission.</param>
|
||||
/// <param name="Description">Description of mission.</param>
|
||||
internal record class Mission(string Name, string Description);
|
||||
208
12_Bombs_Away/csharp/BombsAwayGame/MissionSide.cs
Normal file
208
12_Bombs_Away/csharp/BombsAwayGame/MissionSide.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a protagonist that chooses a standard (non-kamikaze) mission.
|
||||
/// </summary>
|
||||
internal abstract class MissionSide : Side
|
||||
{
|
||||
/// <summary>
|
||||
/// Create instance using the given UI.
|
||||
/// </summary>
|
||||
/// <param name="ui">UI to use.</param>
|
||||
public MissionSide(IUserInterface ui)
|
||||
: base(ui)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasonable upper bound for missions flown previously.
|
||||
/// </summary>
|
||||
private const int MaxMissionCount = 160;
|
||||
|
||||
/// <summary>
|
||||
/// Choose a mission and attempt it. If attempt fails, perform an enemy counterattack.
|
||||
/// </summary>
|
||||
public override void Play()
|
||||
{
|
||||
Mission mission = ChooseMission();
|
||||
UI.Output(mission.Description);
|
||||
|
||||
int missionCount = MissionCountFromUI();
|
||||
CommentOnMissionCount(missionCount);
|
||||
|
||||
AttemptMission(missionCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Choose a mission.
|
||||
/// </summary>
|
||||
/// <returns>Mission chosen.</returns>
|
||||
private Mission ChooseMission()
|
||||
{
|
||||
IList<Mission> missions = AllMissions;
|
||||
string[] missionNames = missions.Select(a => a.Name).ToArray();
|
||||
int index = UI.Choose(ChooseMissionMessage, missionNames);
|
||||
return missions[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to display when choosing a mission.
|
||||
/// </summary>
|
||||
protected abstract string ChooseMissionMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All aviailable missions to choose from.
|
||||
/// </summary>
|
||||
protected abstract IList<Mission> AllMissions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get mission count from UI. If mission count exceeds a reasonable maximum, ask UI again.
|
||||
/// </summary>
|
||||
/// <returns>Mission count from UI.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display a message about the given mission count, if it is unusually high or low.
|
||||
/// </summary>
|
||||
/// <param name="missionCount">Mission count to comment on.</param>
|
||||
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?");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt mission.
|
||||
/// </summary>
|
||||
/// <param name="missionCount">Number of missions previously flown. Higher mission counts will yield a higher probability of success.</param>
|
||||
private void AttemptMission(int missionCount)
|
||||
{
|
||||
if (missionCount < RandomInteger(0, MaxMissionCount))
|
||||
{
|
||||
MissedTarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
MissionSucceeded();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display message indicating that target was missed. Choose enemy artillery and perform a counterattack.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Choose enemy artillery from UI.
|
||||
/// </summary>
|
||||
/// <returns>Artillery chosen.</returns>
|
||||
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];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum allowed hit rate percent.
|
||||
/// </summary>
|
||||
private const int MinEnemyHitRatePercent = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed hit rate percent.
|
||||
/// </summary>
|
||||
private const int MaxEnemyHitRatePercent = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Get the enemy hit rate percent from UI. Value must be between zero and <see cref="MaxEnemyHitRatePercent"/>.
|
||||
/// If value is less than <see cref="MinEnemyHitRatePercent"/>, mission fails automatically because the user is
|
||||
/// assumed to be untruthful.
|
||||
/// </summary>
|
||||
/// <returns>Enemy hit rate percent from UI.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
97
12_Bombs_Away/csharp/BombsAwayGame/Side.cs
Normal file
97
12_Bombs_Away/csharp/BombsAwayGame/Side.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace BombsAwayGame;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a protagonist in the game.
|
||||
/// </summary>
|
||||
internal abstract class Side
|
||||
{
|
||||
/// <summary>
|
||||
/// Create instance using the given UI.
|
||||
/// </summary>
|
||||
/// <param name="ui">UI to use.</param>
|
||||
public Side(IUserInterface ui)
|
||||
{
|
||||
UI = ui;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play this side.
|
||||
/// </summary>
|
||||
public abstract void Play();
|
||||
|
||||
/// <summary>
|
||||
/// User interface supplied to ctor.
|
||||
/// </summary>
|
||||
protected IUserInterface UI { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Random-number generator for this play-through.
|
||||
/// </summary>
|
||||
private readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random floating-point number greater than or equal to zero, and less than one.
|
||||
/// </summary>
|
||||
/// <returns>Random floating-point number greater than or equal to zero, and less than one.</returns>
|
||||
protected double RandomFrac() => _random.NextDouble();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random integer in a range.
|
||||
/// </summary>
|
||||
/// <param name="minValue">The inclusive lower bound of the number returned.</param>
|
||||
/// <param name="maxValue">The exclusive upper bound of the number returned.</param>
|
||||
/// <returns>Random integer in a range.</returns>
|
||||
protected int RandomInteger(int minValue, int maxValue) => _random.Next(minValue: minValue, maxValue: maxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Display messages indicating the mission succeeded.
|
||||
/// </summary>
|
||||
protected void MissionSucceeded()
|
||||
{
|
||||
UI.Output("DIRECT HIT!!!! " + RandomInteger(0, 100) + " KILLED.");
|
||||
UI.Output("MISSION SUCCESSFUL.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Guns type of enemy artillery.
|
||||
/// </summary>
|
||||
protected EnemyArtillery Guns { get; } = new("GUNS", 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Missiles type of enemy artillery.
|
||||
/// </summary>
|
||||
protected EnemyArtillery Missiles { get; } = new("MISSILES", 35);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Both Guns and Missiles type of enemy artillery.
|
||||
/// </summary>
|
||||
protected EnemyArtillery Both { get; } = new("BOTH", 35);
|
||||
|
||||
/// <summary>
|
||||
/// Perform enemy counterattack using the given artillery and hit rate percent.
|
||||
/// </summary>
|
||||
/// <param name="artillery">Enemy artillery to use.</param>
|
||||
/// <param name="hitRatePercent">Hit rate percent for enemy.</param>
|
||||
protected void EnemyCounterattack(EnemyArtillery artillery, int hitRatePercent)
|
||||
{
|
||||
if (hitRatePercent + artillery.Accuracy > RandomInteger(0, 100))
|
||||
{
|
||||
MissionFailed();
|
||||
}
|
||||
else
|
||||
{
|
||||
UI.Output("YOU MADE IT THROUGH TREMENDOUS FLAK!!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display messages indicating the mission failed.
|
||||
/// </summary>
|
||||
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...");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user