Merge pull request #305 from pgruderman/combat

Ported Combat to C#
This commit is contained in:
Jeff Atwood
2021-06-21 11:10:31 -07:00
committed by GitHub
13 changed files with 815 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -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.

View File

@@ -0,0 +1,42 @@
using System;
namespace Game
{
/// <summary>
/// Represents the armed forces for a country.
/// </summary>
public record ArmedForces
{
/// <summary>
/// Gets the number of men and women in the army.
/// </summary>
public int Army { get; init; }
/// <summary>
/// Gets the number of men and women in the navy.
/// </summary>
public int Navy { get; init; }
/// <summary>
/// Gets the number of men and women in the air force.
/// </summary>
public int AirForce { get; init; }
/// <summary>
/// Gets the total number of troops in the armed forces.
/// </summary>
public int TotalTroops => Army + Navy + AirForce;
/// <summary>
/// Gets the number of men and women in the given branch.
/// </summary>
public int this[MilitaryBranch branch] =>
branch switch
{
MilitaryBranch.Army => Army,
MilitaryBranch.Navy => Navy,
MilitaryBranch.AirForce => AirForce,
_ => throw new ArgumentException("INVALID BRANCH")
};
}
}

View File

@@ -0,0 +1,60 @@
using System;
namespace Game
{
/// <summary>
/// Represents the state of the game after reaching a ceasefire.
/// </summary>
public sealed class Ceasefire : WarState
{
/// <summary>
/// Gets a flag indicating whether the player achieved absolute victory.
/// </summary>
public override bool IsAbsoluteVictory { get; }
/// <summary>
/// Gets the outcome of the war.
/// </summary>
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;
}
}
/// <summary>
/// Initializes a new instance of the Ceasefire class.
/// </summary>
/// <param name="computerForces">
/// The computer's forces.
/// </param>
/// <param name="playerForces">
/// The player's forces.
/// </param>
/// <param name="absoluteVictory">
/// Indicates whether the player acheived absolute victory (defeating
/// the computer without destroying its military).
/// </param>
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");
}
}

View File

@@ -0,0 +1,102 @@
using System;
namespace Game
{
/// <summary>
/// Contains functions for interacting with the user.
/// </summary>
public class Controller
{
/// <summary>
/// Gets the player's initial armed forces distribution.
/// </summary>
/// <param name="computerForces">
/// The computer's initial armed forces.
/// </param>
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;
}
/// <summary>
/// Gets the military branch for the user's next attack.
/// </summary>
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
};
}
/// <summary>
/// Gets a valid attack size from the player for the given branch
/// of the armed forces.
/// </summary>
/// <param name="troopsAvailable">
/// The number of troops available.
/// </param>
public static int GetAttackSize(int troopsAvailable)
{
var attackSize = 0;
do
{
View.PromptAttackSize();
attackSize = InputInteger();
}
while (attackSize < 0 || attackSize > troopsAvailable);
return attackSize;
}
/// <summary>
/// Gets an integer value from the user.
/// </summary>
public static int InputInteger()
{
var value = default(int);
while (!Int32.TryParse(Console.ReadLine(), out value))
View.PromptValidInteger();
return value;
}
}
}

View File

@@ -0,0 +1,121 @@
namespace Game
{
/// <summary>
/// Represents the state of the game during the final campaign of the war.
/// </summary>
public sealed class FinalCampaign : WarState
{
/// <summary>
/// Initializes a new instance of the FinalCampaign class.
/// </summary>
/// <param name="computerForces">
/// The computer's forces.
/// </param>
/// <param name="playerForces">
/// The player's forces.
/// </param>
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."
);
}
}
}
}

View File

@@ -0,0 +1,187 @@
namespace Game
{
/// <summary>
/// Represents the state of the game during the initial campaign of the war.
/// </summary>
public sealed class InitialCampaign : WarState
{
/// <summary>
/// Initializes a new instance of the InitialCampaign class.
/// </summary>
/// <param name="computerForces">
/// The computer's forces.
/// </param>
/// <param name="playerForces">
/// The player's forces.
/// </param>
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."
);
}
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Game
{
/// <summary>
/// Enumerates the different branches of the military.
/// </summary>
public enum MilitaryBranch
{
Army,
Navy,
AirForce
}
}

View File

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

View File

@@ -0,0 +1,110 @@
using System;
namespace Game
{
/// <summary>
/// Contains functions for displaying information to the user.
/// </summary>
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");
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Game
{
/// <summary>
/// Enumerates the possible outcomes of the war.
/// </summary>
public enum WarResult
{
ComputerVictory,
PlayerVictory,
PeaceTreaty
}
}

View File

@@ -0,0 +1,101 @@
using System;
namespace Game
{
/// <summary>
/// Represents the current state of the war.
/// </summary>
public abstract class WarState
{
/// <summary>
/// Gets the computer's armed forces.
/// </summary>
public ArmedForces ComputerForces { get; }
/// <summary>
/// Gets the player's armed forces.
/// </summary>
public ArmedForces PlayerForces { get; }
/// <summary>
/// Gets a flag indicating whether this state represents absolute
/// victory for the player.
/// </summary>
public virtual bool IsAbsoluteVictory => false;
/// <summary>
/// Gets the final outcome of the war.
/// </summary>
/// <remarks>
/// If the war is ongoing, this property will be null.
/// </remarks>
public virtual WarResult? FinalOutcome => null;
/// <summary>
/// Initializes a new instance of the state class.
/// </summary>
/// <param name="computerForces">
/// The computer's forces.
/// </param>
/// <param name="playerForces">
/// The player's forces.
/// </param>
public WarState(ArmedForces computerForces, ArmedForces playerForces) =>
(ComputerForces, PlayerForces) = (computerForces, playerForces);
/// <summary>
/// Launches an attack.
/// </summary>
/// <param name="branch">
/// The branch of the military to use for the attack.
/// </param>
/// <param name="attackSize">
/// The number of men and women to use for the attack.
/// </param>
/// <returns>
/// The new state of the game resulting from the attack and a message
/// describing the result.
/// </returns>
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")
};
/// <summary>
/// Conducts an attack with the player's army.
/// </summary>
/// <param name="attackSize">
/// The number of men and women used in the attack.
/// </param>
/// <returns>
/// The new game state and a message describing the result.
/// </returns>
protected abstract (WarState nextState, string message) AttackWithArmy(int attackSize);
/// <summary>
/// Conducts an attack with the player's navy.
/// </summary>
/// <param name="attackSize">
/// The number of men and women used in the attack.
/// </param>
/// <returns>
/// The new game state and a message describing the result.
/// </returns>
protected abstract (WarState nextState, string message) AttackWithNavy(int attackSize);
/// <summary>
/// Conducts an attack with the player's air force.
/// </summary>
/// <param name="attackSize">
/// The number of men and women used in the attack.
/// </param>
/// <returns>
/// The new game state and a message describing the result.
/// </returns>
protected abstract (WarState nextState, string message) AttackWithAirForce(int attackSize);
}
}