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