Retro computing: Hammurabi - CheshireChris - 09-28-2024 12:02 PM
I was inspired by Mike's recent posting of a Prime version of the 1970s mainframe "Trek" to have a go myself at porting something to the Prime. This is my first attempt at Prime programming in PPL and is a version of the classic resource management game "Hammurabi", in which the player must run ancient Sumeria for a ten-year term of office. Each year you must decide how much land to buy or sell, how much grain to feed your people, and how many acres of land to plant with seed. If you don't feed your people enough, some will starve and there is a risk of plague. If you starve too many people you will be impeached and thrown out of office before the end of the game! If you survive your ten-your term, you'll be presented with an assessment of your performance.
The program was converted from a BASIC listing in David Ahl's public domain book, "BASIC Computer Games, Microcomputer Edition", which can be downloaded from the Internet Archive, and I found the process of converting it from BASIC to PPL relatively straightforward. It took about eight hours' work from start to finish, and I understand a lot now about PPL programming than I did when I started the task! I've added a lot of comments to the source code so if you want to look at the source you should find it straightforward. I would encourage you NOT to look at the source code before playing, because it gives away the winning strategy. The program is 354 lines long and 21KB in size.
To install the program, simply copy the "Hammurabi.hpprgm" file to the "Programs" list on your Prime and run it either from the Program Catalog (Shift+1) or just by typing "Hammurabi" from the Home screen. All output is to the Prime's terminal. A line appears at the bottom of the terminal when input is required: enter a number and press the ENTER key. To exit at any time, press the ESC key.
As I say, this is my first attempt at Prime programming. All constructive feedback is very welcome!
Code:
// Hammurabi
// Converted from a BASIC program in David Ahl's
// "BASIC Computer Games, Microcomputer Edition"
// by Chris Marriott.
//
// Revision history:
// 28 Sep 24: Initial release.
// Constants controlling model behaviour
CONST gameYears:=10; // number of years to run the model
CONST bushelsPerPerson:=20; // bushels to feed one person for a year
CONST acresPerPerson:=10; // acres a person can sow
CONST impeachPercent:=45; // percentage of deaths triggering impeachment
CONST plaguePercent:=15; // % probability of plague
CONST STATUS_OK:=0; // OK to continue
CONST STATUS_QUIT:=1; // end the game
// Declare variables
population; // current population
incomers; // people coming to the city
bushels; // bushels in the treasury
acres; // acres owned
acresPlanted; // acres planted
bushelsFed; // bushels fed to the people
eaten; // bushels eaten by rats
harvest; // bushels harvested
harvestYield; // bushels per acre harvested
deaths; // deaths this year
totalDeaths; // total number of deaths
deathPercent; // average % deaths
year; // game year
plague; // probability of plague
status; // game status
// Main program
EXPORT Hammurabi()
BEGIN
// Initialise data and display the introductory text
InitData();
Intro();
// Continue until we either reach the time limit, or
// we are impeached.
WHILE year<gameYears AND status==STATUS_OK DO
// Display the start of year status
AnnualReport();
// Buy or sell land
BuySellLand();
// Feed the people
FeedPeople();
// Plant crops
PlantCrops();
// Harvest time
HarvestCrops();
// End of year processing
YearEnd();
END;
// On a normal exit, display the final year's stats
// and a performance report. If we exited due to being
// impeached, display the impeachment report.
IF status==STATUS_OK THEN
AnnualReport();
FinalStatus();
ELSE
PRINT("\nPress any key to continue...");
PRINT();
PRINT("You starved "+deaths+" people in one year!");
Impeach();
END;
PRINT("\nPress any key to exit.");
RETURN 0;
END;
// Display the introductory screen
Intro()
BEGIN
PRINT();
PRINT("HAMMURABI\n\n");
PRINT("Try your hand at governing ancient Sumeria for");
PRINT("a "+gameYears+"-year term of office. Each year, you must");
PRINT("decide how much land to buy or sell, how much");
PRINT("grain to feed your people, and how much to");
PRINT("plant in the fields. The consequences of failure");
PRINT("are severe!");
PRINT("\nConverted from David Ahl's \"BASIC Computer");
PRINT("Games\" by Chris Marriott.");
PRINT("\nPress any key to begin...");
WAIT();
END;
// Initialise game data
InitData()
BEGIN
status:=STATUS_OK;
year:=0;
population:=95;
deaths:=0;
totalDeaths:=0;
deathPercent:=0;
bushels:=2800;
harvest:=3000;
eaten:=harvest-bushels;
harvestYield:=3;
acres:=harvest/harvestYield;
incomers:=5;
plague:=100; // no plague the first year!
END;
// Display the report for the current year
AnnualReport()
BEGIN
// Increment the year
year:=year+1;
PRINT();
PRINT("Hammurabi, I beg to report to you,");
PRINT("In year "+year+", "+deaths+" people starved, "+incomers+" came to the city.");
population:=population+incomers;
// If the plague has struck, halve the population
IF plague<plaguePercent THEN
population:=FLOOR(population/2);
PRINT("A horrible plague struck! Half the people died.");
END;
PRINT("Population is now "+population);
PRINT("The city now owns "+acres+" acres.");
PRINT("You harvested "+harvestYield+" bushels per acre.");
PRINT("Rats ate "+eaten+" bushels.");
PRINT("You now have "+bushels+" bushels in store.");
PRINT("\n");
END;
// Buy or sell land
BuySellLand()
BEGIN
LOCAL landPrice, trade, totalCost;
// Generate the price of land this year
landPrice:= RANDINT(17,26);
// Do we want to buy land?
REPEAT
PRINT("Land is trading at "+landPrice+" bushels per acre.");
PRINT("How many acres do you wish to buy?");
trade:=ReadNum();
totalCost:=landPrice*trade;
IF totalCost>bushels THEN
TooExpensive();
END;
UNTIL totalCost<=bushels;
IF trade<>0 THEN
acres:=acres+trade;
bushels:=bushels-totalCost;
// If we haven't bought land, do we want to sell?
ELSE
REPEAT
PRINT("How many acres do you wish to sell?");
trade:=ReadNum();
IF trade>acres THEN
TooMuchLand();
END;
UNTIL trade<=acres;
acres:=acres-trade;
bushels:=bushels+landPrice*trade;
END;
END;
// Decide how much grain to feed the people
FeedPeople()
BEGIN
REPEAT
PRINT("How many bushels do you wish to feed your people?");
bushelsFed:=ReadNum();
IF bushelsFed>bushels THEN
TooExpensive();
END
UNTIL bushelsFed<=bushels;
bushels:=bushels-bushelsFed;
END;
// Decide how many acres to plant
PlantCrops()
BEGIN
LOCAL OK;
REPEAT
OK:=1;
PRINT("How many acres do you wish to plant with seed?");
acresPlanted:=ReadNum();
// More acres that you own?
IF acresPlanted>acres THEN
TooMuchLand();
OK:=0;
END;
// Enough grain for seed?
IF OK==1 AND FLOOR(acresPlanted/2)>bushels THEN
TooExpensive();
OK:=0;
END;
// Enough people to tend the crops?
IF OK==1 AND acresPlanted>acresPerPerson*population THEN
PRINT("But you only have "+population+" people to tend the fields! Now then,");
OK:=0;
END;
UNTIL OK==1;
// Remove the planted grain from the treasury
bushels:=bushels-FLOOR(acresPlanted/2);
END;
// Harvest this year's crops
HarvestCrops()
BEGIN
LOCAL rnd;
// Random harvest yield
harvestYield:=RANDINT(1,5);
harvest:=acresPlanted*harvestYield;
// Rats?
rnd:=RANDINT(1,5);
IF FLOOR(rnd/2)==rnd/2 THEN
eaten:=FLOOR(bushels/rnd);
ELSE
eaten:=0;
END;
bushels:=bushels-eaten+harvest;
END;
// End of year processing
YearEnd()
BEGIN
LOCAL peopleFed, rnd;
// How many people have come to the city?
rnd:=RANDINT(1,5);
incomers:=FLOOR(rnd*(20*acres+bushels)/population/100+1);
// How many people had full tummies?
peopleFed:=FLOOR(bushelsFed/bushelsPerPerson);
// Calculate the probability of plague.
plague:=RANDOM(0,100);
// If the population is greater than the number of people
// fed, people will starve
IF population>peopleFed THEN
deaths:=population-peopleFed;
IF deaths>impeachPercent/100*population THEN
status:=STATUS_QUIT;
END;
deathPercent:=((year-1)*deathPercent+deaths*100/population)/year;
population:=peopleFed;
totalDeaths:=totalDeaths+deaths;
ELSE
// We've fed everyone
deaths:=0;
END;
END;
// Final status report
FinalStatus()
BEGIN
LOCAL acrespp, summary;
PRINT("\nPress any key to continue...");
WAIT();
PRINT();
PRINT("In your term of office, "+FLOOR(deathPercent)+"% of the population starved on average.");
PRINT("A total of "+totalDeaths+" people died!");
acrespp:=acres/population;
PRINT("You started with 10 acres per person and ended with "+FLOOR(acrespp)+" acres per person.");
summary:=0;
IF deathPercent>33 OR acrespp<7 THEN
Impeach();
summary:=1;
END;
IF summary==0 AND (deathPercent>10 OR acrespp<9) THEN
PRINT("Your heavy-handed performance smacks of Nero and Ivan IV. The people (remaining) find you an unpleasant ruler and, frankly, hate your guts!");
summary:=1;
END;
IF summary==0 AND acrespp<10 THEN
PRINT("Your performance could have been somewhat better, but really wasn't too bad at all. "+FLOOR(population*0.8*RANDOM())+" people would dearly like to see you assassinated, but we all have our trivial problems.");
summary:=1;
END;
IF summary==0 THEN
PRINT("A fantastic performance! Charlemagne, Disraeli and Jefferson combined could not have done better!");
END;
END;
// You've been impeached
Impeach()
BEGIN
PRINT("Due to this extreme mismanagement you have not only been impeached and thrown out of office, but you have also been declared a national disgrace!");
END;
// Not enough grain to do this
TooExpensive()
BEGIN
PRINT("Hammurabi, think again! You have only "+bushels+" bushels. Now then,");
END;
// Not enough land to do this
TooMuchLand()
BEGIN
PRINT("Hammurabi, think again! You own only "+acres+" acres. Now then,");
END;
// Read a line from the terminal, echo the input, and return a numeric value
ReadNum()
BEGIN
LOCAL input;
input:=READLINE();
PRINT(input);
RETURN EXPR(input);
END;
RE: Retro computing: Hammurabi - CheshireChris - 09-28-2024 12:13 PM
Sorry - posting the source code has made the message very wide. I didn't know that would happen. Hope you can read it OK.
RE: Retro computing: Hammurabi - komame - 09-28-2024 06:33 PM
Hi Chris,
Very nice, but I see one small problem. The program crashes when an invalid value is entered. It happened to me by accident, and a Syntax Error occurred, causing the program to terminate without the possibility of continuing. When you get data using READLINE, you should validate its correctness. In your case, you always expect a positive integer, so it's easy to check whether the given value is a valid number.
Instead of writing:
Code:
ReadNum()
BEGIN
LOCAL input;
input := READLINE();
PRINT(input);
RETURN EXPR(input);
END;
you should check if the entered value is numeric and re-prompt with READLINE if the value is incorrect:
Code:
Conv_positive_num(t)
BEGIN
t:=SUPPRESS(t," ");
local i,s=SIZE(t);
FOR i FROM 1 TO s DO
IF t[i]<48 OR t[i]>57 THEN
RETURN -1; // invalid value
END;
END;
RETURN EXPR(t);
END;
ReadNum()
BEGIN
LOCAL inp,num;
REPEAT
inp:=READLINE();
num:=Conv_positive_num(inp);
IF num == -1 THEN
PRINT("Invalid value (" + inp + "). Try again.");
END;
UNTIL num <> -1;
PRINT(num);
RETURN num;
END;
The code is longer, but now the program doesn't crash when an incorrect value is entered by accident. Additionally, EXPR treats spaces as multiplication, for example, 2 [space] 240 (if you wanted to write "2 240", it would be treated as 2*240, which results in 480). However, using the conversion as shown above, spaces are removed, so "2 240" will be 2240 as expected.
RE: Retro computing: Hammurabi - CheshireChris - 09-28-2024 06:35 PM
Thanks, Piotr!
|