Java Notes
Programming: Hammurabi v3
Description
Extend a program to simulate the functioning of an early agricultural society. It is based on the ancient computer game Hammurabi, named after a Babylonian king (See en.wikipedia.org/wiki/Hammurabi) famous for his laws. He also ran a very authoritarian society in which the peasants were allocated fixed rations of food by the state.
Goal. The user, who has just become the ruler of a kingdom, wants to make a place in history by having having the largest population of peasants. The simulation will last ten years or until there are no peasants or food left.
Make a Plan, Simulate one year, get a Report. The ruler must make a plan each year which tells how much grain to feed the peasants and how much to use in planting next year's crop.
Grain is the basic resource. Make a plan each year by asking the ruler how to use the grain that is in storage.
- How many bushels to feed the people.
- How many bushels to use as seed for planting next year's crop.
Unused grain is saved for next year in case of a bad harvest.
Initial Conditions
The program starts with Kingdom having an area of 1000 acres, a population of 100, and with 3000 bushels of grain from the previous harvest.
Food and Population Rules
Each person needs a minimum of 20 bushels of grain per year to survive.
Starvation. If the ruler doesn't allocate enough food for everyone, some will starve or emmigrate. The population is then reduced by the number of people who couldn't be fed.
Immigrants if more food. If people receive more than 20 bushels per person, immigrants sufficient to eat that extra food will come from neighboring kingdoms, resulting in a population increase.
Population Formula. The size of the population can be computed by simply dividing the total amount of food by the amount needed per person.
myPopulation = food / FOOD_REQUIRED_PER_PERSON;
This is inside the simulateOneYear
method,
where myPopulation
is an instance variable representing
the Kingdom's current population, and FOOD_REQUIRED_PER_PERSON
is a constant predefined to be 20.
For example, if the ruler allocates 2400 bushels, this will support a population of 2400 / 20, which is 120. This would become the new population.
Agriculture Rules
Seed for Planting. Not all grain can be used for feeding the people. Some must be used to plant next year's crop. It takes two bushels of grain to plant an acre. To plant everything therefore requires 2 * area bushels of grain. But there also must be a sufficient number of peasants to farm the land.
Harvest variation. There are variations in the weather each year. The yield varies from randomly from year to year.
Separation of concerns - The purpose of each class is different
One of the purposes of separating a program into different classes is to group data and methods that belong together and not mix the different responsibilities of the classes. This has been divided into three classes:
- HammurabiGame - Main program and user interface I/O.
- OneYearPlan - Holds the relevant values for ruler's plan for next year.
- Kingdom - Represents the important properties and functioning of a kingdom.
HammurabiGame class
This contains the main program, which creates a new Kingdom, gets information from the user/ruler on how to allocate the grain each year, simulates one year, and display the results for that year. It continues in a loop until the end of the simulation time period or everyone has starved or there is no more food. It should know nothing about the internals of the Kingdom class; it's communication is only by means of calling a Kingdom object's public methods or accessing it's public constants.
The Kingdom class represents the information about a kingdom, and provides methods for getting this information. In addition, it provides a method that simulates one year given the amount of grain to be used for food and seed. The simulation updates instance variables to reflect what happened during that year. This class must not contain user interface operations -- no input / output. All interaction with the user must be in the calling program in the HammurabiGame class.
The HammurabiGame class first version
This class runs the simulation and provides the user interface. It contains the main method. It creates a Kingdom object called babylonia, but you can change that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
// File : oop/hammurabi/HammurabiGame.java // Purpose: Simulate running a kingdom. // Author : Fred Swartz - Placed in the public domain. // Date : 04 May 2006 import javax.swing.*; public class HammurabiGame { //============================================================= constants private static final int LENGTH_OF_SIMULATION = 10; //=================================================================== main public static void main(String[] args) { //... Initialization. Kingdom babylonia = new Kingdom(); while (babylonia.getYear() < LENGTH_OF_SIMULATION && babylonia.getGrain() > 0 && babylonia.getPopulation() > 0) { //... Get the ruler's plan. OneYearPlan thisYearsPlan = createPlan(babylonia); //... Simulate one year babylonia.simulateOneYear(thisYearsPlan); } //... Final report to the ruler. JOptionPane.showMessageDialog(null, "End of simulation." + babylonia); if (babylonia.getPopulation() > 0 && babylonia.getGrain() > 0) { JOptionPane.showMessageDialog(null, "We have survived because of your brillance, " + "Most Exhalted Ruler!"); } else { JOptionPane.showMessageDialog(null, "Perhaps you should choose another occupation."); } } //============================================================= createPlan // Prompt the user for planned amount of grain to use for food and seed // that is needed for the next year. private static OneYearPlan createPlan(Kingdom country) { //... 1. Get the amount of grain to feed peasants. int maintenanceLevelFood = country.getAmountOfFoodNeeded(); int availableGrain = country.getGrain(); String message = "Exalted Ruler, It is time to make a one-year plan." + "\n\n The status of the Kingdom is " + country.toString() + "\n\nHow much of the " + availableGrain + " bushels do you wish to feed the " + country.getPopulation() + " peasants?" + "\n\n" + maintenanceLevelFood + " bushels are required to avoid starvation." + "\nMore food it will increase immigration." + "\nIf you hit return, either everyone will be fed, " + "or all food will be used."; int defaultFood = Math.min(availableGrain, maintenanceLevelFood); int food = getInt(message, 0, availableGrain, defaultFood); //... 2. Get the amount of grain to use for seed. // Take the minimum of the amount of remaining // grain, and the maximum plantable. int maxSeed = Math.min(availableGrain - food, country.getMaximumPlantableSeed()); message = "Exalted Ruler, how many of the plantable " + maxSeed + " bushels should be used for seed?" + "\nIf you hit return, the maximum plantable value will be used."; int seeds = getInt(message, 0, maxSeed, maxSeed); //... 3. Create a one-year plan and return it. OneYearPlan nextPlan = new OneYearPlan(food, seeds, 0); return nextPlan; } //================================================================= getInt // Utility method to prompt the user for an integer in a range. // If the user enters an empty string (ie, just hits return), // the default value is used. // Doesn't handle null (CANCEL) or bad input. private static int getInt(String prompt, int min, int max, int defaultValue) { int result; do { String strVal = JOptionPane.showInputDialog(null, prompt); if (strVal.equals("")) { //... If user simply hits enter with nothing, use default value. result = defaultValue; } else { result = Integer.parseInt(strVal); if (result < min || result > max) { JOptionPane.showMessageDialog(null, "ERROR: Input must be between " + min + " and " + max); } } } while (result < min || result > max); return result; } } |
The Kingdom class first version
There are two portions to a class: data (instance variables) and methods.
This class also has "getters" to get instance variable values.
The simulateOneYear
method, which takes parameters for
how much grain to use to plant the next crop, and how much grain to feed
the population.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
// File : oop/hammurabi/Kingdom.java // Purpose: Represents a "kingdom". // Author : Fred Swartz - Placed in the public domain. // Date : 05 May 2006 class Kingdom { //============================================================== constants public static final int MAX_ACRES_FARMABLE_PER_PERSON = 10; public static final int PRICE_PER_ACRE = 20; // Price in bushels of grain. public static final int SEED_REQUIRED_PER_ACRE = 2; public static final int FOOD_REQUIRED_PER_PERSON = 20; public static final int MIN_YIELD = 3; public static final int MAX_YIELD = 7; //===================================================== instance variables private int myPopulation = 100; // Initial number of peasants. private int myGrain = 3000; // Initial bushels of grain. private int myArea = 1000; // Initial acres of land. private int myYear = 0; // How many years has Kingdom been ruled. //... Used only for reporting (via toString) private String myPopulationGraph = ""; //============================================================ constructor public Kingdom() { //... Start the population graph at myPopulationGraph = createBar(myPopulation/10, "x"); } //========================================================== getPopulation public int getPopulation() { return myPopulation; } //=============================================================== getGrain public int getGrain() { return myGrain; } //================================================================ getArea public int getArea() { return myArea; } //================================================================ getYear public int getYear() { return myYear; } //================================================== getAmountOfFoodNeeded // A convenience method for computing food needed for user interface. public int getAmountOfFoodNeeded() { return myPopulation * FOOD_REQUIRED_PER_PERSON; } //================================================ getMaximumPlantableSeed // Compute max bushels of seed that can be used given area and peasants. public int getMaximumPlantableSeed() { int peasantFarmableArea = myPopulation * MAX_ACRES_FARMABLE_PER_PERSON; int maxFarmableArea = Math.min(myArea, peasantFarmableArea); return SEED_REQUIRED_PER_ACRE * maxFarmableArea; } //======================================================== simulateOneYear // Give the one-year plan, simulate what happens using that plan. // This uses the amount of grain allocated for food and seed with // rules for how these are applied to update the population and grain. public void simulateOneYear(OneYearPlan plan) { int foodAllocation = plan.getFood(); int seed = plan.getSeed(); //... Don't allow illegal parameter values. // Bad values should have been corrected in the user interface. if (foodAllocation < 0 || seed < 0) { throw new IllegalArgumentException("Negative values. Food=" + foodAllocation + " seed=" + seed); } if (foodAllocation + seed > myGrain) { throw new IllegalArgumentException("Too much grain used. Food=" + foodAllocation + " seed=" + seed); } //... Compute new population based on allocated food. myPopulation = foodAllocation / FOOD_REQUIRED_PER_PERSON; myGrain -= foodAllocation; // Remove food grain from storage. //... Get seed for planting. Take the smaller of the amount // the ruler supplied, and the amount that can actually be // used based on the area and population.- int usableSeed = Math.min(seed, getMaximumPlantableSeed()); myGrain -= usableSeed; // Remove seed grain from storage. //... Compute (random) harvest. int acresPlanted = usableSeed / SEED_REQUIRED_PER_ACRE; int yield = rand(MIN_YIELD, MAX_YIELD); int harvest = yield * acresPlanted; //... Add the new harvest to the grain in storage. myGrain += harvest; //... Update the population history graph. myPopulationGraph += "\n" + createBar(myPopulation/10, "x"); myYear++; // Another year has passed. } //=============================================================== toString // The toString method will authomatically be called whenever Java // needs the String version of a Kingdom object. @Override public String toString() { String result = ""; result = "\nYear " + myYear + "\nPopulation graph:\n" + myPopulationGraph + "\nPopulation: " + myPopulation + "\n Grain: " + myGrain + "\n Area: " + myArea; return result; } //============================================================== createBar // Utility method to create bar of repeating characters for graph. private String createBar(int size, String c) { String result = ""; for (int i = 0; i < size; i++) { result += c; } return result; } //=================================================================== rand // Utility method to generate random int from low to high. private int rand(int low, int high) { return low + (int)((high-low+1) * Math.random()); } } |
OneYearPlan class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// File : oop/hammurabi/OneYearPlan.java - // Purpose: This stores the parameters that will be used to simulate // one year in a Kingdom. // Author : Fred Swartz - Placed in the public domain. // Date : 04 May 2006 public class OneYearPlan { int myPlannedFoodAllocation; int myPlannedSeedAllocation; int myPlannedLandTransaction; // >0 is a purchase. <0 is sale. //============================================================ constructor /** Creates a new instance of OneYearPlan */ public OneYearPlan(int food, int seeds, int landPurchaseOrSale) { myPlannedFoodAllocation = food; myPlannedSeedAllocation = seeds; myPlannedLandTransaction = landPurchaseOrSale; } //================================================================ getters public int getFood() { return myPlannedFoodAllocation; } public int getSeed() { return myPlannedSeedAllocation; } public int getLandChange() { return myPlannedLandTransaction; } } |
Extension possibilities
This games allows many interesting extensions. After you get these files running (should be easy), make some of the following extensions. Altho some extensions change only one class, you should try to make changes in all three classes.
- Better interface. Don't hesitate to make the interface better (HammurabiGame class).
- Buying and selling land. Land can be bought from the neighboring kingdoms (or sold back to them) for 20 bushels per acre. The yearly plan that the ruler makes should therefore include how much land to buy/sell in addition to the food and seed allocations. Allowing land to be added is the only way to get the population to really grow. This requires changes in HammurabiGame class to prompt the user for how much land to buy/sell and the Kingdom class to carry out this action. The OneYearPlan class already has land in it, although it is not used by the other classes.
- Revolt. If more than 45% of the population starves, there is a revolt which overthrows the ruler, and the game ends.
-
Add soldiers to this program.
- Keep track of how many soldiers the Kingdom has.
- Write a "getter" method so the user interface can find out how many there are.
- Update the toString() method so that it reports the number of soldiers.
- Add an extra parameter to the
simulateOneYear
method to indicate how much grain should be used to feed soldiers. It's unwise to not feed the soldiers enought! - Soldiers eat twice as much as peasants.
- Soldiers don't do any farming (ie, no changes to farming computation).
- For every 10 soldiers, you get an extra acre of land each year because of border disputes.
- Luxuries. Any normal despotic ruler is going to think first about himself. Perhaps the goal of the game should be to accumulate yachts, palaces, or whatever. You can change the game for this purpose.
- Create an automated ruler consultant. The ruler knows he's unable to
make really good decisions about running his kingdom. Your job is to write
a class, Consultant, that has has a
createPlan
method that makes a plan for the kingdom. - Additional random fluctuations
- Rats. Each year, rats eat a random amount from 0-10% of your grain
- Plague. There is a 15% chance each year of a plague. If that happens, half the population dies.
- Make this into a two person game. Create two Kingdoms, using the same
class, but just call the constructor twice. For example,
Kingdom akkad = new Kingdom(); Kingdom summaria = new Kingdom();
In the main input look have each "ruler" answer the questions for his kingdom. As soon as one kingdom becomes much more populous or rich or whatever, it takes over the weaker kingdom.