From 74f758fd35eb27e4a0817531d7684448ca00920b Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 3 Apr 2021 02:21:06 -0400 Subject: [PATCH] Ported Hammurabi to C# --- 43 Hammurabi/csharp/Game.csproj | 8 + 43 Hammurabi/csharp/Hammurabi.sln | 25 +++ 43 Hammurabi/csharp/src/ActionResult.cs | 37 ++++ 43 Hammurabi/csharp/src/Controller.cs | 65 ++++++ 43 Hammurabi/csharp/src/GameResult.cs | 45 ++++ 43 Hammurabi/csharp/src/GameState.cs | 73 +++++++ 43 Hammurabi/csharp/src/GreatOffence.cs | 12 ++ 43 Hammurabi/csharp/src/PerformanceRating.cs | 14 ++ 43 Hammurabi/csharp/src/Program.cs | 51 +++++ 43 Hammurabi/csharp/src/Rules.cs | 207 +++++++++++++++++++ 43 Hammurabi/csharp/src/View.cs | 193 +++++++++++++++++ 11 files changed, 730 insertions(+) create mode 100644 43 Hammurabi/csharp/Game.csproj create mode 100644 43 Hammurabi/csharp/Hammurabi.sln create mode 100644 43 Hammurabi/csharp/src/ActionResult.cs create mode 100644 43 Hammurabi/csharp/src/Controller.cs create mode 100644 43 Hammurabi/csharp/src/GameResult.cs create mode 100644 43 Hammurabi/csharp/src/GameState.cs create mode 100644 43 Hammurabi/csharp/src/GreatOffence.cs create mode 100644 43 Hammurabi/csharp/src/PerformanceRating.cs create mode 100644 43 Hammurabi/csharp/src/Program.cs create mode 100644 43 Hammurabi/csharp/src/Rules.cs create mode 100644 43 Hammurabi/csharp/src/View.cs diff --git a/43 Hammurabi/csharp/Game.csproj b/43 Hammurabi/csharp/Game.csproj new file mode 100644 index 00000000..20827042 --- /dev/null +++ b/43 Hammurabi/csharp/Game.csproj @@ -0,0 +1,8 @@ + + + + Exe + net5.0 + + + diff --git a/43 Hammurabi/csharp/Hammurabi.sln b/43 Hammurabi/csharp/Hammurabi.sln new file mode 100644 index 00000000..feeae212 --- /dev/null +++ b/43 Hammurabi/csharp/Hammurabi.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31129.286 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Game", "Game.csproj", "{20599300-7C6E-48A2-AB24-EC7CCF224A5C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {20599300-7C6E-48A2-AB24-EC7CCF224A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20599300-7C6E-48A2-AB24-EC7CCF224A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20599300-7C6E-48A2-AB24-EC7CCF224A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20599300-7C6E-48A2-AB24-EC7CCF224A5C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {89DBE213-C0F0-4ABA-BB2D-5D9AAED41FF6} + EndGlobalSection +EndGlobal diff --git a/43 Hammurabi/csharp/src/ActionResult.cs b/43 Hammurabi/csharp/src/ActionResult.cs new file mode 100644 index 00000000..58d4f3cb --- /dev/null +++ b/43 Hammurabi/csharp/src/ActionResult.cs @@ -0,0 +1,37 @@ +namespace Hammurabi +{ + /// + /// Enumerates the different possible outcomes of attempting the various + /// actions in the game. + /// + public enum ActionResult + { + /// + /// The action was a success. + /// + Success, + + /// + /// The action could not be completed because the city does not have + /// enough bushels of grain. + /// + InsufficientStores, + + /// + /// The action could not be completed because the city does not have + /// sufficient acreage. + /// + InsufficientLand, + + /// + /// The action could not be completed because the city does not have + /// sufficient population. + /// + InsufficientPopulation, + + /// + /// The requested action offended the city steward. + /// + Offense + } +} diff --git a/43 Hammurabi/csharp/src/Controller.cs b/43 Hammurabi/csharp/src/Controller.cs new file mode 100644 index 00000000..91d1b64f --- /dev/null +++ b/43 Hammurabi/csharp/src/Controller.cs @@ -0,0 +1,65 @@ +using System; + +namespace Hammurabi +{ + /// + /// Provides methods for reading input from the user. + /// + public static class Controller + { + /// + /// Continuously prompts the user to enter a number until he or she + /// enters a valid number and updates the game state. + /// + /// + /// The current game state. + /// + /// + /// Action that will display the prompt to the user. + /// + /// + /// The rule to invoke once input is retrieved. + /// + /// + /// The updated game state. + /// + public static GameState TryUntilSuccess( + GameState state, + Action prompt, + Func rule) + { + while (true) + { + prompt(); + + if (!Int32.TryParse(Console.ReadLine(), out var amount)) + { + View.ShowInvalidNumber(); + } + else + { + var (newState, result) = rule(state, amount); + + switch (result) + { + case ActionResult.InsufficientLand: + View.ShowInsufficientLand(state); + break; + case ActionResult.InsufficientPopulation: + View.ShowInsufficientPopulation(state); + break; + case ActionResult.InsufficientStores: + View.ShowInsufficientStores(state); + break; + case ActionResult.Offense: + // Not sure why we have to blow up the game here... + // Maybe this made sense in the 70's. + throw new GreatOffence(); + default: + return newState; + } + } + } + } + } +} diff --git a/43 Hammurabi/csharp/src/GameResult.cs b/43 Hammurabi/csharp/src/GameResult.cs new file mode 100644 index 00000000..23dfbe28 --- /dev/null +++ b/43 Hammurabi/csharp/src/GameResult.cs @@ -0,0 +1,45 @@ +namespace Hammurabi +{ + /// + /// Stores the final game result. + /// + public record GameResult + { + /// + /// Gets the player's performance rating. + /// + public PerformanceRating Rating { get; init; } + + /// + /// Gets the number of acres in the city per person. + /// + public int AcresPerPerson { get; init; } + + /// + /// Gets the number of people who starved the final year in office. + /// + public int FinalStarvation { get; init; } + + /// + /// Gets the total number of people who starved. + /// + public int TotalStarvation { get; init; } + + /// + /// Gets the average starvation rate per year (as a percentage + /// of population). + /// + public int AverageStarvationRate { get; init; } + + /// + /// Gets the number of people who want to assassinate the player. + /// + public int Assassins { get; init; } + + /// + /// Gets a flag indicating whether the player was impeached for + /// starving too many people. + /// + public bool WasImpeached { get; init; } + } +} diff --git a/43 Hammurabi/csharp/src/GameState.cs b/43 Hammurabi/csharp/src/GameState.cs new file mode 100644 index 00000000..3e2d1eb2 --- /dev/null +++ b/43 Hammurabi/csharp/src/GameState.cs @@ -0,0 +1,73 @@ +namespace Hammurabi +{ + /// + /// Stores the state of the game. + /// + public record GameState + { + /// + /// Gets the current game year. + /// + public int Year { get; init; } + + /// + /// Gets the city's population. + /// + public int Population { get; init; } + + /// + /// Gets the population increase this year. + /// + public int PopulationIncrease { get; init; } + + /// + /// Gets the number of people who starved. + /// + public int Starvation { get; init; } + + /// + /// Gets the city's size in acres. + /// + public int Acres { get; init; } + + /// + /// Gets the price for an acre of land (in bushels). + /// + public int LandPrice { get; init; } + + /// + /// Gets the number of bushels of grain in the city stores. + /// + public int Stores { get; init; } + + /// + /// Gets the amount of food distributed to the people. + /// + public int FoodDistributed { get; init; } + + /// + /// Gets the number of acres that were planted. + /// + public int AcresPlanted { get; init; } + + /// + /// Gets the number of bushels produced per acre. + /// + public int Productivity { get; init; } + + /// + /// Gets the amount of food lost to rats. + /// + public int Spoilage { get; init; } + + /// + /// Gets a flag indicating whether the current year is a plague year. + /// + public bool IsPlagueYear { get; init; } + + /// + /// Gets a flag indicating whether the player has been impeached. + /// + public bool IsImpeached { get; init; } + } +} diff --git a/43 Hammurabi/csharp/src/GreatOffence.cs b/43 Hammurabi/csharp/src/GreatOffence.cs new file mode 100644 index 00000000..98791492 --- /dev/null +++ b/43 Hammurabi/csharp/src/GreatOffence.cs @@ -0,0 +1,12 @@ +using System; + +namespace Hammurabi +{ + /// + /// Indicates that the game cannot continue due to the player's extreme + /// incompetance and/or unserious attitude! + /// + public class GreatOffence : InvalidOperationException + { + } +} diff --git a/43 Hammurabi/csharp/src/PerformanceRating.cs b/43 Hammurabi/csharp/src/PerformanceRating.cs new file mode 100644 index 00000000..39454720 --- /dev/null +++ b/43 Hammurabi/csharp/src/PerformanceRating.cs @@ -0,0 +1,14 @@ +namespace Hammurabi +{ + /// + /// Enumerates the different performance ratings that the player can + /// achieve. + /// + public enum PerformanceRating + { + Disgraceful, + Bad, + Ok, + Terrific + } +} diff --git a/43 Hammurabi/csharp/src/Program.cs b/43 Hammurabi/csharp/src/Program.cs new file mode 100644 index 00000000..45d5b101 --- /dev/null +++ b/43 Hammurabi/csharp/src/Program.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Immutable; + +namespace Hammurabi +{ + public static class Program + { + public const int GameLength = 10; + + public static void Main(string[] args) + { + var random = new Random ((int) (DateTime.UtcNow.Ticks / 10000)) ; + var state = Rules.BeginGame(); + var history = ImmutableList.Empty; + + View.ShowBanner(); + + try + { + while (state.Year <= GameLength && !state.IsImpeached) + { + state = Rules.BeginTurn(state, random); + View.ShowCitySummary(state); + + View.ShowLandPrice(state); + var newState = Controller.TryUntilSuccess(state, View.PromptBuyLand, Rules.BuyLand); + state = newState.Acres != state.Acres ? + newState : Controller.TryUntilSuccess(state, View.PromptSellLand, Rules.SellLand); + + View.ShowSeparator(); + state = Controller.TryUntilSuccess(state, View.PromptFeedPeople, Rules.FeedPeople); + + View.ShowSeparator(); + state = Controller.TryUntilSuccess(state, View.PromptPlantCrops, Rules.PlantCrops); + + state = Rules.EndTurn(state, random); + history = history.Add(state); + } + + var result = Rules.GetGameResult(history, random); + View.ShowGameResult(result); + } + catch (GreatOffence) + { + View.ShowGreatOffence(); + } + + View.ShowFarewell(); + } + } +} diff --git a/43 Hammurabi/csharp/src/Rules.cs b/43 Hammurabi/csharp/src/Rules.cs new file mode 100644 index 00000000..56a59d96 --- /dev/null +++ b/43 Hammurabi/csharp/src/Rules.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Hammurabi +{ + public static class Rules + { + /// + /// Creates the initial state for a new game. + /// + public static GameState BeginGame() => + new GameState + { + Year = 1, + Population = 95, + PopulationIncrease = 5, + Starvation = 0, + Acres = 1000, + Stores = 0, + AcresPlanted = 1000, + Productivity = 3, + Spoilage = 200, + IsPlagueYear = false, + IsImpeached = false + }; + + /// + /// Updates the game state to start a new turn. + /// + public static GameState BeginTurn(GameState state, Random random) => + state with + { + Population = (state.Population + state.PopulationIncrease - state.Starvation) / (state.IsPlagueYear ? 2 : 1), + LandPrice = random.Next(10) + 17, + Stores = state.Stores + (state.AcresPlanted * state.Productivity) - state.Spoilage, + AcresPlanted = 0, + FoodDistributed = 0 + }; + + /// + /// Attempts to purchase the given number of acres. + /// + /// + /// The updated game state and action result. + /// + public static (GameState newState, ActionResult result) BuyLand(GameState state, int amount) + { + var price = state.LandPrice * amount; + + if (price < 0) + return (state, ActionResult.Offense); + else + if (price > state.Stores) + return (state, ActionResult.InsufficientStores); + else + return (state with { Acres = state.Acres + amount, Stores = state.Stores - price }, ActionResult.Success); + } + + /// + /// Attempts to sell the given number of acres. + /// + /// + /// The updated game state and action result. + /// + public static (GameState newState, ActionResult result) SellLand(GameState state, int amount) + { + var price = state.LandPrice * amount; + + if (price < 0) + return (state, ActionResult.Offense); + else + if (amount >= state.Acres) + return (state, ActionResult.InsufficientLand); + else + return (state with { Acres = state.Acres - amount, Stores = state.Stores + price }, ActionResult.Success); + } + + /// + /// Attempts to feed the people the given number of buschels. + /// + /// + /// + /// The updated game state and action result. + /// + public static (GameState newState, ActionResult result) FeedPeople(GameState state, int amount) + { + if (amount < 0) + return (state, ActionResult.Offense); + else + if (amount > state.Stores) + return (state, ActionResult.InsufficientStores); + else + return (state with { Stores = state.Stores - amount, FoodDistributed = state.FoodDistributed + amount }, ActionResult.Success); + } + + /// + /// Attempts to plant crops on the given number of acres. + /// + /// + /// The updated game state and action result. + /// + public static (GameState newState, ActionResult result) PlantCrops(GameState state, int amount) + { + var storesRequired = amount / 2; + var maxAcres = state.Population * 10; + + if (amount < 0) + return (state, ActionResult.Offense); + else + if (amount > state.Acres) + return (state, ActionResult.InsufficientLand); + else + if (storesRequired > state.Stores) + return (state, ActionResult.InsufficientStores); + else + if ((state.AcresPlanted + amount) > maxAcres) + return (state, ActionResult.InsufficientPopulation); + else + return (state with + { + AcresPlanted = state.AcresPlanted + amount, + Stores = state.Stores - storesRequired, + }, ActionResult.Success); + } + + /// + /// Ends the current turn and returns the updated game state. + /// + public static GameState EndTurn(GameState state, Random random) + { + var productivity = random.Next(1, 6); + var harvest = productivity * state.AcresPlanted; + + var spoilage = random.Next(1, 6) switch + { + 2 => state.Stores / 2, + 4 => state.Stores / 4, + _ => 0 + }; + + var populationIncrease= (int)((double) random.Next(1, 6) * (20 * state.Acres + state.Stores + harvest - spoilage) / state.Population / 100 + 1); + + var plagueYear = random.Next(20) < 3; + + var peopleFed = state.FoodDistributed / 20; + var starvation = peopleFed < state.Population ? state.Population - peopleFed : 0; + var impeached = starvation > state.Population * 0.45; + + return state with + { + Year = state.Year + 1, + Productivity = productivity, + Spoilage = spoilage, + PopulationIncrease = populationIncrease, + Starvation = starvation, + IsPlagueYear = plagueYear, + IsImpeached = impeached + }; + } + + /// + /// Examines the game's history to arrive at the final result. + /// + public static GameResult GetGameResult(IEnumerable history, Random random) + { + var (_, averageStarvationRate, totalStarvation, finalState) = history.Aggregate( + (count: 0, starvationRate: 0, totalStarvation: 0, finalState: default(GameState)), + (stats, state) => + ( + stats.count + 1, + ((stats.starvationRate * stats.count) + (state.Starvation * 100 / state.Population)) / (stats.count + 1), + stats.totalStarvation + state.Starvation, + state + )); + + var acresPerPerson = finalState.Acres / finalState.Population; + + var rating = finalState.IsImpeached ? + PerformanceRating.Disgraceful : + (averageStarvationRate, acresPerPerson) switch + { + (> 33, _) => PerformanceRating.Disgraceful, + (_, < 7) => PerformanceRating.Disgraceful, + (> 10, _) => PerformanceRating.Bad, + (_, < 9) => PerformanceRating.Bad, + (> 3, _) => PerformanceRating.Ok, + (_, < 10) => PerformanceRating.Ok, + _ => PerformanceRating.Terrific + }; + + var assassins = rating == PerformanceRating.Ok ? + random.Next(0, (int)(finalState.Population * 0.8)) : 0; + + return new GameResult + { + Rating = rating, + AcresPerPerson = acresPerPerson, + FinalStarvation = finalState.Starvation, + TotalStarvation = totalStarvation, + AverageStarvationRate = averageStarvationRate, + Assassins = assassins, + WasImpeached = finalState.IsImpeached + }; + } + } +} diff --git a/43 Hammurabi/csharp/src/View.cs b/43 Hammurabi/csharp/src/View.cs new file mode 100644 index 00000000..be10af32 --- /dev/null +++ b/43 Hammurabi/csharp/src/View.cs @@ -0,0 +1,193 @@ +using System; + +namespace Hammurabi +{ + /// + /// Provides various methods for presenting information to the user. + /// + public static class View + { + /// + /// Shows the introductory banner to the player. + /// + public static void ShowBanner() + { + Console.WriteLine(" HAMURABI"); + Console.WriteLine(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("TRY YOUR HAND AT GOVERNING ANCIENT SUMERIA"); + Console.WriteLine("FOR A TEN-YEAR TERM OF OFFICE."); + } + + /// + /// Shows a summary of the current state of the city. + /// + public static void ShowCitySummary(GameState state) + { + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("HAMURABI: I BEG TO REPORT TO YOU,"); + Console.WriteLine($"IN YEAR {state.Year}, {state.Starvation} PEOPLE STARVED, {state.PopulationIncrease} CAME TO THE CITY,"); + + if (state.IsPlagueYear) + { + Console.WriteLine("A HORRIBLE PLAGUE STRUCK! HALF THE PEOPLE DIED."); + } + + Console.WriteLine($"POPULATION IS NOW {state.Population}"); + Console.WriteLine($"THE CITY NOW OWNS {state.Acres} ACRES."); + Console.WriteLine($"YOU HARVESTED {state.Productivity} BUSHELS PER ACRE."); + Console.WriteLine($"THE RATS ATE {state.Spoilage} BUSHELS."); + Console.WriteLine($"YOU NOW HAVE {state.Stores} BUSHELS IN STORE."); + Console.WriteLine(); + } + + /// + /// Shows the current cost of land. + /// + /// + public static void ShowLandPrice(GameState state) + { + Console.WriteLine ($"LAND IS TRADING AT {state.LandPrice} BUSHELS PER ACRE."); + } + + /// + /// Displays a section separator. + /// + public static void ShowSeparator() + { + Console.WriteLine(); + } + + /// + /// Inform the player that he or she has entered an invalid number. + /// + public static void ShowInvalidNumber() + { + Console.WriteLine("PLEASE ENTER A VALID NUMBER"); + } + + /// + /// Inform the player that he or she has insufficient acreage. + /// + public static void ShowInsufficientLand(GameState state) + { + Console.WriteLine($"HAMURABI: THINK AGAIN. YOU OWN ONLY {state.Acres} ACRES. NOW THEN,"); + } + + /// + /// Inform the player that he or she has insufficient population. + /// + public static void ShowInsufficientPopulation(GameState state) + { + Console.WriteLine($"BUT YOU HAVE ONLY {state.Population} PEOPLE TO TEND THE FIELDS! NOW THEN,"); + } + + /// + /// Inform the player that he or she has insufficient grain stores. + /// + public static void ShowInsufficientStores(GameState state) + { + Console.WriteLine("HAMURABI: THINK AGAIN. YOU HAVE ONLY"); + Console.WriteLine($"{state.Stores} BUSHELS OF GRAIN. NOW THEN,"); + } + + /// + /// Show the player that he or she has caused great offence. + /// + public static void ShowGreatOffence() + { + Console.WriteLine(); + Console.WriteLine("HAMURABI: I CANNOT DO WHAT YOU WISH."); + Console.WriteLine("GET YOURSELF ANOTHER STEWARD!!!!!"); + } + + /// + /// Shows the game's final result to the user. + /// + public static void ShowGameResult(GameResult result) + { + if (!result.WasImpeached) + { + Console.WriteLine($"IN YOUR 10-YEAR TERM OF OFFICE, {result.AverageStarvationRate} PERCENT OF THE"); + Console.WriteLine("POPULATION STARVED PER YEAR ON THE AVERAGE, I.E. A TOTAL OF"); + Console.WriteLine($"{result.TotalStarvation} PEOPLE DIED!!"); + + Console.WriteLine("YOU STARTED WITH 10 ACRES PER PERSON AND ENDED WITH"); + Console.WriteLine($"{result.AcresPerPerson} ACRES PER PERSON."); + Console.WriteLine(); + } + + switch (result.Rating) + { + case PerformanceRating.Disgraceful: + if (result.WasImpeached) + Console.WriteLine($"YOU STARVED {result.FinalStarvation} PEOPLE IN ONE YEAR!!!"); + + Console.WriteLine("DUE TO THIS EXTREME MISMANAGEMENT YOU HAVE NOT ONLY"); + Console.WriteLine("BEEN IMPEACHED AND THROWN OUT OF OFFICE BUT YOU HAVE"); + Console.WriteLine("ALSO BEEN DECLARED NATIONAL FINK!!!!"); + break; + case PerformanceRating.Bad: + Console.WriteLine("YOUR HEAVY-HANDED PERFORMANCE SMACKS OF NERO AND IVAN IV."); + Console.WriteLine("THE PEOPLE (REMIANING) FIND YOU AN UNPLEASANT RULER, AND,"); + Console.WriteLine("FRANKLY, HATE YOUR GUTS!!"); + break; + case PerformanceRating.Ok: + Console.WriteLine("YOUR PERFORMANCE COULD HAVE BEEN SOMEWHAT BETTER, BUT"); + Console.WriteLine($"REALLY WASN'T TOO BAD AT ALL. {result.Assassins} PEOPLE"); + Console.WriteLine("WOULD DEARLY LIKE TO SEE YOU ASSASSINATED BUT WE ALL HAVE OUR"); + Console.WriteLine("TRIVIAL PROBLEMS."); + break; + case PerformanceRating.Terrific: + Console.WriteLine("A FANTASTIC PERFORMANCE!!! CHARLEMANGE, DISRAELI, AND"); + Console.WriteLine("JEFFERSON COMBINED COULD NOT HAVE DONE BETTER!"); + break; + } + } + + /// + /// Shows a farewell message to the user. + /// + public static void ShowFarewell() + { + Console.WriteLine("SO LONG FOR NOW."); + Console.WriteLine(); + } + + /// + /// Prompts the user to buy land. + /// + public static void PromptBuyLand() + { + Console.WriteLine ("HOW MANY ACRES DO YOU WISH TO BUY"); + } + + /// + /// Prompts the user to sell land. + /// + public static void PromptSellLand() + { + Console.WriteLine("HOW MANY ACRES DO YOU WISH TO SELL"); + } + + /// + /// Prompts the user to feed the people. + /// + public static void PromptFeedPeople() + { + Console.WriteLine("HOW MANY BUSHELS DO YOU WISH TO FEED YOUR PEOPLE"); + } + + /// + /// Prompts the user to plant crops. + /// + public static void PromptPlantCrops() + { + Console.WriteLine("HOW MANY ACRES DO YOU WISH TO PLANT WITH SEED"); + } + } +}