James Stanley


Porting Hamurabi to SCAMP

Fri 6 August 2021
Tagged: cpu, software

That's right! 1968's most exciting video game release is coming to 2021's most disappointing CPU architecture. Hamurabi is a single-player text-based game in which you play the leader of ancient Sumeria for 10 years. Each year, you decide how much land to buy or sell, how much food to feed to the population, and how much land to plant with seeds. Occasionally a plague comes along and kills half the population, or rats eat some of the harvest. Land values, harvest yields, and immigration rates fluctuate unpredictably. At the end of the 10 years (if you haven't been forcibly removed from office already) your performance is evaluated.

Here's a short demo of inaccurate play:

It's actually pretty easy to win the game with the highest praise almost every time if you know how it works. Perhaps I'll write up "How to win at Hamurabi" at some point.

If you want, you can play the game online in a handful of places. Probably the most authentic experience is the one on "playold.games". It appears to be a DOS port of the game running in the browser in a javascript DOSbox. Or if you really want, you can clone the SCAMP repo, work out how to build everything, and run hamurabi in the SCAMP emulator. Email me if you can't figure it out.

Porting Hamurabi

The original game was apparently written in FOCAL, although at that time it was called "King of Sumeria" or "The Sumer Game". Its roots can be traced back further, however, to The Sumerian Game from 1964, so it's not entirely obvious to me where Hamurabi can be said to have started. I'd be interested to look at the FOCAL source code, but I couldn't find it. Fortunately, the game has had many ports over the years.

The earliest source code I found was this low-res BASIC listing from the book BASIC Computer Games published in 1978, so that's what I ported to SCAMP, bugs and all. Of course, that's not to say I didn't introduce extra bugs of my own.

The full source of my port is in sys/hamurabi.sl in the SCAMP git repo.

Arithmetic

I was surprised to find that such an old game appears to make extensive use of (presumably) floating point arithmetic, e.g.:

552 D=P-C: IF D>.45*P THEN 560

I don't yet have any fractional number support on either my CPU or programming language, so I converted these calculations into pure integer form for SLANG:

if (mul(D,100) > mul(45,P)) {

(The CPU doesn't natively support multiplication, so neither does the programming language - I considered implementing infix multiplication as syntactic sugar, but decided the explicit function call is fun and quirky. To be honest it was touch and go whether the language would even have a non-buggy magnitude comparison operator...)

Input

At first reading I thought the game systematically omitted question marks on questions:

320 PRINT "HOW MANY ACRES DO YOU WISH TO BUY";
321 INPUT Q: IF Q<0 THEN 850

This presents a real dilemma: I don't want my port to deviate from historical accuracy, but I also don't want to ask questions without question marks.

Fortunately, I later came across a screenshot from the game which clearly shows question marks on questions:


(From Wikipedia)

So I have surmised that the INPUT statement in BASIC inserts its own question mark, and also that the trailing semi-colon after a PRINT statement suppresses a trailing end-line character.

That said, the screenshot clearly shows "O MIGHTY MASTER" wording after "THINK AGAIN", which the BASIC listing definitely doesn't have, so who knows? Maybe the BASIC version actually didn't have question marks.

Comments

The BASIC source code is very sparsely commented, and the comments don't even address my biggest questions. I would have appreciated comments explaining how the calculations are meant to work, and what the single-letter variable names are meant to represent. Instead, these things have to be derived by working backwards from the impact they have on the text output. But at least the immigration calculation has this:

532 REM *** LET'S HAVE SOME BABIES
533 I=INT(C*(20*A+S)/P/100+1)

Control flow

IF statements in this dialect of BASIC appear to function more like a conditional jump than a normal if statement, in that they take a line number as a branch target, rather than a block of code to conditionally execute. This makes the control flow quite hard to follow. The language also doesn't appear to support named functions, although it thankfully does have a RETURN statement, so some form of proto-subroutine is possible.

Example:

410 PRINT "HOW MANY BUSHELS DO YOU WISH TO FEED YOUR PEOPLE";
411 INPUT Q
412 IF Q<0 THEN 850
418 REM *** TRYING TO USE MORE GRAIN THAN IS IN SILOS?
420 IF Q<=S THEN 430
421 GOSUB 710
422 GOTO 410
430 S=S-Q: C=1: PRINT
...
710 PRINT "HAMURABI:  THINK AGAIN. YOU HAVE ONLY"
711 PRINT S;"BUSHELS OF GRAIN.  NOW THEN,"
712 RETURN

Also note that there is nothing at line 410 to mark it as the start of a loop body, or at 710 to draw attention to the fact that this is the entry point of a subroutine. You just have to know. In my opinion, the SLANG port is substantially easier to follow, despite my efforts to mimic the original coding style.

while (1) {
    printf("HOW MANY BUSHELS DO YOU WISH TO FEED YOUR PEOPLE",0);
    Q=input();
    # *** TRYING TO USE MORE GRAIN THAN IS IN SILOS?
    if (Q <= S) break;
    thinkgrain();
};
S=S-Q; C=1;
printf("\n",0);
...
thinkgrain = func() {
    printf("HAMURABI:  THINK AGAIN. YOU HAVE ONLY\n",0);
    printf("%d BUSHELS OF GRAIN.  NOW THEN,\n",[S]);
};

(The IF Q<0 THEN 850 test is moved into the input() function because it's the same on every input - if you try to input a negative number the game tells you "HAMURABI:  I CANNOT DO WHAT YOU WISH. GET YOURSELF ANOTHER STEWARD!!!!!" and you lose)

Random numbers

Random numbers are an important part of the game. They create uncertainty about the future which mimics the real-life conditions that the game is trying to simulate. I don't know how good the BASIC random number generator is, but I know that the SCAMP one is awful. This is the first program I've written for SCAMP that needed random numbers, so I wrote this:

var randstate = 0x5a7f;
var rand = func() {
    randstate = mul(randstate, 17) + 0x2e7;
    return randstate;
};

The seed is the same every time, so it is easy to learn what the land prices and harvest yields are going to be, when there will be a plague, and so on, but it's a start. The form of the RNG (Xn+1 = (a Xn + c) mod m) is a linear congruential generator, but the constants (a=17, c=0x2e7, and implicitly m=0xffff), are chosen by guessing instead of for their good properties. I definitely want a better RNG for SCAMP eventually, but this is adequate to make the game work for now.

I've been thinking a bit about how I might seed the RNG so that it isn't the same every time. It would be quite easy to have the kernel count loop iterations between booting up and the first keyboard input, maybe that's the best way to seed it.

Other SCAMP stuff

Case

I have made a start on the wooden case. Here's some photos of the card cage inside the case, with no cards installed:

I still have some sanding to do to tidy up the joints, but it is roughly what I wanted. Removing the domed nuts at the top allows most of the case to lift off, allowing access to the back and sides of the card cage. The card cage is more permanently bolted to the base piece, which also holds the power socket and power supply.

Originally I was going to put the power switch, reset switch, and clock on a PCB in the card cage, but given that that would only leave 1 spare slot to experiment with, I think I'd rather put them on a separate panel mounted to the bottom of the case, and bodge-wired to the backplane, to leave a 2nd slot free for expansion.

Clock

I experimented with the RC2014 clock card to find out how high I could clock SCAMP without anything going wrong. I was disappointed to find that the limit is about 1 MHz. The next step up available on the RC2014 clock is 1.2 MHz, and at that speed I couldn't get the CompactFlash card to work. I did attempt Bill Shen's suggestion of putting 100 Ohm resistors on the data lines and an RC filter on IORD, but it didn't appear to make any difference. So I've given up for now and I'm just going to tolerate running it at 1 MHz. It would be nice to go faster, but I can always revisit it at a later date. Getting it finished is more important. Rory suggested adding a pull-up/-down resistor to every line on the bus to help the levels transition more quickly. I haven't yet tried this.

Apart from the CompactFlash card, everything else seems to work up to at least 2.5 MHz, and after that point text output stops working, and I can tell from looking at the lights that the CPU isn't working properly either. But if I can fix the CompactFlash problem then 2.5 MHz should be within easy reach. I then don't know which part is making it fall off the rails above 2.5 MHz, and don't know exactly how I'd go about debugging it. Maybe I'd need to attach a pretty wide logic analyser to the bus and see if I can work out what is happening?

Other games

Tetris would be good, to complete the nand2tetris theme. Snake might also work, although it would have to be played on the 80x25 text terminal. I think Colossal Cave Adventure is an almost mandatory port. And a Z-machine implementation would open up a great wealth of other text adventures for free.



If you like my blog, please consider subscribing to the RSS feed or the mailing list: