diff --git a/28 Combat/csharp/Combat.sln b/28 Combat/csharp/Combat.sln
new file mode 100644
index 00000000..522b680e
--- /dev/null
+++ b/28 Combat/csharp/Combat.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31321.278
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Game", "Game.csproj", "{054A1718-1B7D-4954-81A7-EEA390713439}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {054A1718-1B7D-4954-81A7-EEA390713439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {054A1718-1B7D-4954-81A7-EEA390713439}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {054A1718-1B7D-4954-81A7-EEA390713439}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {054A1718-1B7D-4954-81A7-EEA390713439}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1EBA7488-1DA6-4B0B-8234-F10A65E96BDB}
+ EndGlobalSection
+EndGlobal
diff --git a/28 Combat/csharp/Game.csproj b/28 Combat/csharp/Game.csproj
new file mode 100644
index 00000000..dbc7a12e
--- /dev/null
+++ b/28 Combat/csharp/Game.csproj
@@ -0,0 +1,6 @@
+
+
+ Exe
+ net5.0
+
+
diff --git a/28 Combat/csharp/README.md b/28 Combat/csharp/README.md
index 4daabb5c..7634ed9c 100644
--- a/28 Combat/csharp/README.md
+++ b/28 Combat/csharp/README.md
@@ -1,3 +1,7 @@
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/)
+
+The original BASIC code has a surprising number of bugs for such a small program.
+For the sake of preserving the original behaviour, I've left them in place and
+commented the ones I noticed.
diff --git a/28 Combat/csharp/src/ArmedForces.cs b/28 Combat/csharp/src/ArmedForces.cs
new file mode 100644
index 00000000..f246a179
--- /dev/null
+++ b/28 Combat/csharp/src/ArmedForces.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Game
+{
+ ///
+ /// Represents the armed forces for a country.
+ ///
+ public record ArmedForces
+ {
+ ///
+ /// Gets the number of men and women in the army.
+ ///
+ public int Army { get; init; }
+
+ ///
+ /// Gets the number of men and women in the navy.
+ ///
+ public int Navy { get; init; }
+
+ ///
+ /// Gets the number of men and women in the air force.
+ ///
+ public int AirForce { get; init; }
+
+ ///
+ /// Gets the total number of troops in the armed forces.
+ ///
+ public int TotalTroops => Army + Navy + AirForce;
+
+ ///
+ /// Gets the number of men and women in the given branch.
+ ///
+ public int this[MilitaryBranch branch] =>
+ branch switch
+ {
+ MilitaryBranch.Army => Army,
+ MilitaryBranch.Navy => Navy,
+ MilitaryBranch.AirForce => AirForce,
+ _ => throw new ArgumentException("INVALID BRANCH")
+ };
+ }
+}
diff --git a/28 Combat/csharp/src/Ceasefire.cs b/28 Combat/csharp/src/Ceasefire.cs
new file mode 100644
index 00000000..3b1a7d65
--- /dev/null
+++ b/28 Combat/csharp/src/Ceasefire.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Game
+{
+ ///
+ /// Represents the state of the game after reaching a ceasefire.
+ ///
+ public sealed class Ceasefire : WarState
+ {
+ ///
+ /// Gets a flag indicating whether the player achieved absolute victory.
+ ///
+ public override bool IsAbsoluteVictory { get; }
+
+ ///
+ /// Gets the outcome of the war.
+ ///
+ public override WarResult? FinalOutcome
+ {
+ get
+ {
+ if (IsAbsoluteVictory || PlayerForces.TotalTroops > 3 / 2 * ComputerForces.TotalTroops)
+ return WarResult.PlayerVictory;
+ else
+ if (PlayerForces.TotalTroops < 2 / 3 * ComputerForces.TotalTroops)
+ return WarResult.ComputerVictory;
+ else
+ return WarResult.PeaceTreaty;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the Ceasefire class.
+ ///
+ ///
+ /// The computer's forces.
+ ///
+ ///
+ /// The player's forces.
+ ///
+ ///
+ /// Indicates whether the player acheived absolute victory (defeating
+ /// the computer without destroying its military).
+ ///
+ public Ceasefire(ArmedForces computerForces, ArmedForces playerForces, bool absoluteVictory = false)
+ : base(computerForces, playerForces)
+ {
+ IsAbsoluteVictory = absoluteVictory;
+ }
+
+ protected override (WarState nextState, string message) AttackWithArmy(int attackSize) =>
+ throw new InvalidOperationException("THE WAR IS OVER");
+
+ protected override (WarState nextState, string message) AttackWithNavy(int attackSize) =>
+ throw new InvalidOperationException("THE WAR IS OVER");
+
+ protected override (WarState nextState, string message) AttackWithAirForce(int attackSize) =>
+ throw new InvalidOperationException("THE WAR IS OVER");
+ }
+}
diff --git a/28 Combat/csharp/src/Controller.cs b/28 Combat/csharp/src/Controller.cs
new file mode 100644
index 00000000..b02f81ad
--- /dev/null
+++ b/28 Combat/csharp/src/Controller.cs
@@ -0,0 +1,102 @@
+using System;
+
+namespace Game
+{
+ ///
+ /// Contains functions for interacting with the user.
+ ///
+ public class Controller
+ {
+ ///
+ /// Gets the player's initial armed forces distribution.
+ ///
+ ///
+ /// The computer's initial armed forces.
+ ///
+ public static ArmedForces GetInitialForces(ArmedForces computerForces)
+ {
+ var playerForces = default(ArmedForces);
+
+ // BUG: This loop allows the player to assign negative values to
+ // some branches, leading to strange results.
+ do
+ {
+ View.ShowDistributeForces();
+
+ View.PromptArmySize(computerForces.Army);
+ var army = InputInteger();
+
+ View.PromptNavySize(computerForces.Navy);
+ var navy = InputInteger();
+
+ View.PromptAirForceSize(computerForces.AirForce);
+ var airForce = InputInteger();
+
+ playerForces = new ArmedForces
+ {
+ Army = army,
+ Navy = navy,
+ AirForce = airForce
+ };
+ }
+ while (playerForces.TotalTroops > computerForces.TotalTroops);
+
+ return playerForces;
+ }
+
+ ///
+ /// Gets the military branch for the user's next attack.
+ ///
+ public static MilitaryBranch GetAttackBranch(WarState state, bool isFirstTurn)
+ {
+ if (isFirstTurn)
+ View.PromptFirstAttackBranch();
+ else
+ View.PromptNextAttackBranch(state.ComputerForces, state.PlayerForces);
+
+ // If the user entered an invalid branch number in the original
+ // game, the code fell through to the army case. We'll preserve
+ // that behaviour here.
+ return Console.ReadLine() switch
+ {
+ "2" => MilitaryBranch.Navy,
+ "3" => MilitaryBranch.AirForce,
+ _ => MilitaryBranch.Army
+ };
+ }
+
+ ///
+ /// Gets a valid attack size from the player for the given branch
+ /// of the armed forces.
+ ///
+ ///
+ /// The number of troops available.
+ ///
+ public static int GetAttackSize(int troopsAvailable)
+ {
+ var attackSize = 0;
+
+ do
+ {
+ View.PromptAttackSize();
+ attackSize = InputInteger();
+ }
+ while (attackSize < 0 || attackSize > troopsAvailable);
+
+ return attackSize;
+ }
+
+ ///
+ /// Gets an integer value from the user.
+ ///
+ public static int InputInteger()
+ {
+ var value = default(int);
+
+ while (!Int32.TryParse(Console.ReadLine(), out value))
+ View.PromptValidInteger();
+
+ return value;
+ }
+ }
+}
diff --git a/28 Combat/csharp/src/FinalCampaign.cs b/28 Combat/csharp/src/FinalCampaign.cs
new file mode 100644
index 00000000..f34d80e3
--- /dev/null
+++ b/28 Combat/csharp/src/FinalCampaign.cs
@@ -0,0 +1,121 @@
+namespace Game
+{
+ ///
+ /// Represents the state of the game during the final campaign of the war.
+ ///
+ public sealed class FinalCampaign : WarState
+ {
+ ///
+ /// Initializes a new instance of the FinalCampaign class.
+ ///
+ ///
+ /// The computer's forces.
+ ///
+ ///
+ /// The player's forces.
+ ///
+ public FinalCampaign(ArmedForces computerForces, ArmedForces playerForces)
+ : base(computerForces, playerForces)
+ {
+ }
+
+ protected override (WarState nextState, string message) AttackWithArmy(int attackSize)
+ {
+ if (attackSize < ComputerForces.Army / 2)
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces,
+ PlayerForces with
+ {
+ Army = PlayerForces.Army - attackSize
+ }),
+ "I WIPED OUT YOUR ATTACK!"
+ );
+ }
+ else
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces with
+ {
+ Army = 0
+ },
+ PlayerForces),
+ "YOU DESTROYED MY ARMY!"
+ );
+ }
+ }
+
+ protected override (WarState nextState, string message) AttackWithNavy(int attackSize)
+ {
+ if (attackSize < ComputerForces.Navy / 2)
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces,
+ PlayerForces with
+ {
+ Army = PlayerForces.Army / 4,
+ Navy = PlayerForces.Navy / 2
+ }),
+ "I SUNK TWO OF YOUR BATTLESHIPS, AND MY AIR FORCE\n" +
+ "WIPED OUT YOUR UNGAURDED CAPITOL."
+ );
+ }
+ else
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces with
+ {
+ AirForce = 2 * ComputerForces.AirForce / 3,
+ Navy = ComputerForces.Navy / 2
+ },
+ PlayerForces),
+ "YOUR NAVY SHOT DOWN THREE OF MY XIII PLANES,\n" +
+ "AND SUNK THREE BATTLESHIPS."
+ );
+ }
+ }
+
+ protected override (WarState nextState, string message) AttackWithAirForce(int attackSize)
+ {
+ // BUG? Usually, larger attacks lead to better outcomes.
+ // It seems odd that the logic is suddenly reversed here,
+ // but this could be intentional.
+ if (attackSize > ComputerForces.AirForce / 2)
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces,
+ PlayerForces with
+ {
+ Army = PlayerForces.Army / 3,
+ Navy = PlayerForces.Navy / 3,
+ AirForce = PlayerForces.AirForce / 3
+ }),
+ "MY NAVY AND AIR FORCE IN A COMBINED ATTACK LEFT\n" +
+ "YOUR COUNTRY IN SHAMBLES."
+ );
+ }
+ else
+ {
+ return
+ (
+ new Ceasefire(
+ ComputerForces,
+ PlayerForces,
+ absoluteVictory: true),
+ "ONE OF YOUR PLANES CRASHED INTO MY HOUSE. I AM DEAD.\n" +
+ "MY COUNTRY FELL APART."
+ );
+ }
+ }
+ }
+}
diff --git a/28 Combat/csharp/src/InitialCampaign.cs b/28 Combat/csharp/src/InitialCampaign.cs
new file mode 100644
index 00000000..25430074
--- /dev/null
+++ b/28 Combat/csharp/src/InitialCampaign.cs
@@ -0,0 +1,187 @@
+namespace Game
+{
+ ///
+ /// Represents the state of the game during the initial campaign of the war.
+ ///
+ public sealed class InitialCampaign : WarState
+ {
+ ///
+ /// Initializes a new instance of the InitialCampaign class.
+ ///
+ ///
+ /// The computer's forces.
+ ///
+ ///
+ /// The player's forces.
+ ///
+ public InitialCampaign(ArmedForces computerForces, ArmedForces playerForces)
+ : base(computerForces, playerForces)
+ {
+ }
+
+ protected override (WarState nextState, string message) AttackWithArmy(int attackSize)
+ {
+ // BUG: Why are we comparing attack size to the size of our own
+ // military? This leads to some truly absurd results if our
+ // army is tiny.
+ if (attackSize < PlayerForces.Army / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces,
+ PlayerForces with
+ {
+ Army = PlayerForces.Army - attackSize
+ }),
+ $"YOU LOST {attackSize} MEN FROM YOUR ARMY."
+ );
+ }
+ else
+ if (attackSize < 2 * PlayerForces.Army / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ // BUG: Clearly not what we claim below...
+ Army = 0
+ },
+ PlayerForces with
+ {
+ Army = PlayerForces.Army - attackSize / 3
+ }),
+ $"YOU LOST {attackSize / 3} MEN, BUT I LOST {2 * ComputerForces.Army / 3}"
+ );
+ }
+ else
+ {
+ // BUG? This is identical to the third outcome when attacking
+ // with the navy. It seems unlikely that this was the
+ // intent. Probably line 115 in the original source was
+ // supposed to say "GOTO 170" instead of "GOTO 270".
+ // (Line 170 is conspicuously absent.)
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ Navy = 2 * ComputerForces.Navy / 3
+ },
+ PlayerForces with
+ {
+ Army = PlayerForces.Army / 3,
+ AirForce = PlayerForces.AirForce / 3
+ }),
+ "YOU SUNK ONE OF MY PATROL BOATS, BUT I WIPED OUT TWO\n" +
+ "OF YOUR AIR FORCE BASES AND 3 ARMY BASES."
+ );
+ }
+ }
+
+ protected override (WarState nextState, string message) AttackWithNavy(int attackSize)
+ {
+ if (attackSize < ComputerForces.Navy / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces,
+ PlayerForces with
+ {
+ Navy = PlayerForces.Navy - attackSize
+ }),
+ "YOUR ATTACK WAS STOPPED!"
+ );
+ }
+ else
+ if (attackSize < 2 * ComputerForces.Navy / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ Navy = ComputerForces.Navy / 3
+ },
+ PlayerForces),
+ $"YOU DESTROYED {2 * ComputerForces.Navy / 3} OF MY ARMY."
+ );
+ }
+ else
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ Navy = 2 * ComputerForces.Navy / 3
+ },
+ PlayerForces with
+ {
+ Army = PlayerForces.Army / 3,
+ AirForce = PlayerForces.AirForce / 3
+ }),
+ "YOU SUNK ONE OF MY PATROL BOATS, BUT I WIPED OUT TWO\n" +
+ "OF YOUR AIR FORCE BASES AND 3 ARMY BASES."
+ );
+ }
+ }
+
+ protected override (WarState nextState, string message) AttackWithAirForce(int attackSize)
+ {
+ // BUG: Why are we comparing the attack size to the size of
+ // our own air force? Surely we meant to compare to the
+ // computer's air force.
+ if (attackSize < PlayerForces.AirForce / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces,
+ PlayerForces with
+ {
+ AirForce = PlayerForces.AirForce - attackSize
+ }),
+ "YOUR ATTACK WAS WIPED OUT."
+ );
+ }
+ else
+ if (attackSize < 2 * PlayerForces.AirForce / 3)
+ {
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ Army = 2 * ComputerForces.Army / 3,
+ Navy = ComputerForces.Navy / 3,
+ AirForce = ComputerForces.AirForce / 3
+ },
+ PlayerForces),
+ "WE HAD A DOGFIGHT. YOU WON - AND FINISHED YOUR MISSION."
+ );
+ }
+ else
+ {
+
+ return
+ (
+ new FinalCampaign(
+ ComputerForces with
+ {
+ Army = 2 * ComputerForces.Army / 3
+ },
+ PlayerForces with
+ {
+ Army = PlayerForces.Army / 4,
+ Navy = PlayerForces.Navy / 3
+ }),
+ "YOU WIPED OUT ONE OF MY ARMY PATROLS, BUT I DESTROYED" +
+ "TWO NAVY BASES AND BOMBED THREE ARMY BASES."
+ );
+ }
+ }
+ }
+}
diff --git a/28 Combat/csharp/src/MilitaryBranch.cs b/28 Combat/csharp/src/MilitaryBranch.cs
new file mode 100644
index 00000000..50f53c5e
--- /dev/null
+++ b/28 Combat/csharp/src/MilitaryBranch.cs
@@ -0,0 +1,12 @@
+namespace Game
+{
+ ///
+ /// Enumerates the different branches of the military.
+ ///
+ public enum MilitaryBranch
+ {
+ Army,
+ Navy,
+ AirForce
+ }
+}
diff --git a/28 Combat/csharp/src/Program.cs b/28 Combat/csharp/src/Program.cs
new file mode 100644
index 00000000..1695867d
--- /dev/null
+++ b/28 Combat/csharp/src/Program.cs
@@ -0,0 +1,31 @@
+namespace Game
+{
+ class Program
+ {
+ static void Main()
+ {
+ View.ShowBanner();
+ View.ShowInstructions();
+
+ var computerForces = new ArmedForces { Army = 30000, Navy = 20000, AirForce = 22000 };
+ var playerForces = Controller.GetInitialForces(computerForces);
+
+ var state = (WarState) new InitialCampaign(computerForces, playerForces);
+ var isFirstTurn = true;
+
+ while (!state.FinalOutcome.HasValue)
+ {
+ var branch = Controller.GetAttackBranch(state, isFirstTurn);
+ var attackSize = Controller.GetAttackSize(state.PlayerForces[branch]);
+
+ var (nextState, message) = state.LaunchAttack(branch, attackSize);
+ View.ShowMessage(message);
+
+ state = nextState;
+ isFirstTurn = false;
+ }
+
+ View.ShowResult(state);
+ }
+ }
+}
diff --git a/28 Combat/csharp/src/View.cs b/28 Combat/csharp/src/View.cs
new file mode 100644
index 00000000..199194fd
--- /dev/null
+++ b/28 Combat/csharp/src/View.cs
@@ -0,0 +1,110 @@
+using System;
+
+namespace Game
+{
+ ///
+ /// Contains functions for displaying information to the user.
+ ///
+ public static class View
+ {
+ public static void ShowBanner()
+ {
+ Console.WriteLine(" COMBAT");
+ Console.WriteLine(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY");
+ }
+
+ public static void ShowInstructions()
+ {
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine();
+ Console.WriteLine("I AM AT WAR WITH YOU.");
+ Console.WriteLine("WE HAVE 72000 SOLDIERS APIECE.");
+ }
+
+ public static void ShowDistributeForces()
+ {
+ Console.WriteLine();
+ Console.WriteLine("DISTRIBUTE YOUR FORCES.");
+ Console.WriteLine("\tME\t YOU");
+ }
+
+ public static void ShowMessage(string message)
+ {
+ Console.WriteLine(message);
+ }
+
+ public static void ShowResult(WarState finalState)
+ {
+ if (!finalState.IsAbsoluteVictory)
+ {
+ Console.WriteLine();
+ Console.WriteLine("FROM THE RESULTS OF BOTH OF YOUR ATTACKS,");
+ }
+
+ switch (finalState.FinalOutcome)
+ {
+ case WarResult.ComputerVictory:
+ Console.WriteLine("YOU LOST-I CONQUERED YOUR COUNTRY. IT SERVES YOU");
+ Console.WriteLine("RIGHT FOR PLAYING THIS STUPID GAME!!!");
+ break;
+ case WarResult.PlayerVictory:
+ Console.WriteLine("YOU WON, OH! SHUCKS!!!!");
+ break;
+ case WarResult.PeaceTreaty:
+ Console.WriteLine("THE TREATY OF PARIS CONCLUDED THAT WE TAKE OUR");
+ Console.WriteLine("RESPECTIVE COUNTRIES AND LIVE IN PEACE.");
+ break;
+ }
+ }
+
+ public static void PromptArmySize(int computerArmySize)
+ {
+ Console.Write($"ARMY\t{computerArmySize}\t? ");
+ }
+
+ public static void PromptNavySize(int computerNavySize)
+ {
+ Console.Write($"NAVY\t{computerNavySize}\t? ");
+ }
+
+ public static void PromptAirForceSize(int computerAirForceSize)
+ {
+ Console.Write($"A. F.\t{computerAirForceSize}\t? ");
+ }
+
+ public static void PromptFirstAttackBranch()
+ {
+ Console.WriteLine("YOU ATTACK FIRST. TYPE (1) FOR ARMY; (2) FOR NAVY;");
+ Console.WriteLine("AND (3) FOR AIR FORCE.");
+ Console.Write("? ");
+ }
+
+ public static void PromptNextAttackBranch(ArmedForces computerForces, ArmedForces playerForces)
+ {
+ // BUG: More of a nit-pick really, but the order of columns in the
+ // table is reversed from what we showed when distributing troops.
+ // The tables should be consistent.
+ Console.WriteLine();
+ Console.WriteLine("\tYOU\tME");
+ Console.WriteLine($"ARMY\t{playerForces.Army}\t{computerForces.Army}");
+ Console.WriteLine($"NAVY\t{playerForces.Navy}\t{computerForces.Navy}");
+ Console.WriteLine($"A. F.\t{playerForces.AirForce}\t{computerForces.AirForce}");
+
+ Console.WriteLine("WHAT IS YOUR NEXT MOVE?");
+ Console.WriteLine("ARMY=1 NAVY=2 AIR FORCE=3");
+ Console.Write("? ");
+ }
+
+ public static void PromptAttackSize()
+ {
+ Console.WriteLine("HOW MANY MEN");
+ Console.Write("? ");
+ }
+
+ public static void PromptValidInteger()
+ {
+ Console.WriteLine("ENTER A VALID INTEGER VALUE");
+ }
+ }
+}
diff --git a/28 Combat/csharp/src/WarResult.cs b/28 Combat/csharp/src/WarResult.cs
new file mode 100644
index 00000000..a491fe4b
--- /dev/null
+++ b/28 Combat/csharp/src/WarResult.cs
@@ -0,0 +1,14 @@
+namespace Game
+{
+ ///
+ /// Enumerates the possible outcomes of the war.
+ ///
+ public enum WarResult
+ {
+ ComputerVictory,
+
+ PlayerVictory,
+
+ PeaceTreaty
+ }
+}
diff --git a/28 Combat/csharp/src/WarState.cs b/28 Combat/csharp/src/WarState.cs
new file mode 100644
index 00000000..1bc5f6cd
--- /dev/null
+++ b/28 Combat/csharp/src/WarState.cs
@@ -0,0 +1,101 @@
+using System;
+
+namespace Game
+{
+ ///
+ /// Represents the current state of the war.
+ ///
+ public abstract class WarState
+ {
+ ///
+ /// Gets the computer's armed forces.
+ ///
+ public ArmedForces ComputerForces { get; }
+
+ ///
+ /// Gets the player's armed forces.
+ ///
+ public ArmedForces PlayerForces { get; }
+
+ ///
+ /// Gets a flag indicating whether this state represents absolute
+ /// victory for the player.
+ ///
+ public virtual bool IsAbsoluteVictory => false;
+
+ ///
+ /// Gets the final outcome of the war.
+ ///
+ ///
+ /// If the war is ongoing, this property will be null.
+ ///
+ public virtual WarResult? FinalOutcome => null;
+
+ ///
+ /// Initializes a new instance of the state class.
+ ///
+ ///
+ /// The computer's forces.
+ ///
+ ///
+ /// The player's forces.
+ ///
+ public WarState(ArmedForces computerForces, ArmedForces playerForces) =>
+ (ComputerForces, PlayerForces) = (computerForces, playerForces);
+
+ ///
+ /// Launches an attack.
+ ///
+ ///
+ /// The branch of the military to use for the attack.
+ ///
+ ///
+ /// The number of men and women to use for the attack.
+ ///
+ ///
+ /// The new state of the game resulting from the attack and a message
+ /// describing the result.
+ ///
+ public (WarState nextState, string message) LaunchAttack(MilitaryBranch branch, int attackSize) =>
+ branch switch
+ {
+ MilitaryBranch.Army => AttackWithArmy(attackSize),
+ MilitaryBranch.Navy => AttackWithNavy(attackSize),
+ MilitaryBranch.AirForce => AttackWithAirForce(attackSize),
+ _ => throw new ArgumentException("INVALID BRANCH")
+ };
+
+ ///
+ /// Conducts an attack with the player's army.
+ ///
+ ///
+ /// The number of men and women used in the attack.
+ ///
+ ///
+ /// The new game state and a message describing the result.
+ ///
+ protected abstract (WarState nextState, string message) AttackWithArmy(int attackSize);
+
+ ///
+ /// Conducts an attack with the player's navy.
+ ///
+ ///
+ /// The number of men and women used in the attack.
+ ///
+ ///
+ /// The new game state and a message describing the result.
+ ///
+ protected abstract (WarState nextState, string message) AttackWithNavy(int attackSize);
+
+ ///
+ /// Conducts an attack with the player's air force.
+ ///
+ ///
+ /// The number of men and women used in the attack.
+ ///
+ ///
+ /// The new game state and a message describing the result.
+ ///
+ protected abstract (WarState nextState, string message) AttackWithAirForce(int attackSize);
+ }
+}