HAMURABI.BAS

July 27, 2010

At slightly over two hundred lines, not counting the random number generator from the Standard Prelude, this is a big program, so we leave a lot of details unmentioned. But though it is large, none of the pieces are hard, so we can rush right through. We begin at the beginning, with the function that prints the welcome message:

(define (welcome)
  (define (spaces n) (make-string n #\space))
  (for-each display `(
    ,(spaces 32) "HAMURABI" #\newline ,(spaces 15)
    "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"
    #\newline #\newline #\newline #\newline
    "TRY YOUR HAND AT GOVERNING ANCIENT SUMERIA" #\newline
    "FOR A TEN-YEAR TERM OF OFFICE." #\newline #\newline)))

Here is the main routine, which controls the progress of the game and makes all the calculations:

(define (hamurabi)

  (welcome)

  (let ((done?       #f)  ; #t when game is done, else #f
        (pop        100)  ; current population, each needs 20 bushels/year
        (births       5)  ; number of births in current year
        (deaths       0)  ; number of deaths in current year
        (total-deaths 0)  ; cumulative deaths in all years
        (pcnt-starved 0)  ; cumulative percentage starved
        (yield        3)  ; current-year harvest in bushels per acre
        (rats       200)  ; bushels eaten by rats in current year
        (stores    2800)  ; current bushels in stores
        (acres     1000)  ; current acres owned by city
        (buy-sell     0)  ; current year change in acres
        (feed         0)  ; bushels used to feed people in current year
        (plant        0)  ; acres planted in current year, each half bushel
        (plague?     #f)) ; 15% chance of plague in any year, half die

    (let loop ((year 1))  ; main loop starts at end of first year

      ; check for plague, make report
      (when plague? (set! pop (quotient pop 2)))
      (report year deaths births plague? pop acres yield rats stores)

      ; normal termination
      (when (< 10 year)
        (set! done? #t)
        (terminate pcnt-starved total-deaths acres pop))

      ; buy or sell acreage
      (when (not done?)
        (let ((price (get-price)))
          (set! buy-sell (get-buy-sell price stores acres))
          (cond (buy-sell (set! acres (+ acres buy-sell))
                          (set! stores (- stores (* price buy-sell))))
                (else (set! done? #t) (quit)))))

      ; feed people
      (when (not done?)
        (set! feed (get-feed stores))
        (cond (feed (set! stores (- stores feed)))
              (else (set! done? #t) (quit))))

      ; harvest
      (when (not done?)
        (set! plant (get-plant acres stores pop))
        (cond (plant (set! stores (- stores (quotient plant 2)))
                     (set! yield (randint 5 0))
                     (set! rats (let ((c (randint 5 0)))
                       (if (odd? c) 0 (quotient stores c))))
                     (set! stores (+ stores (- rats) (* yield plant))))
              (else (set! done? #t) (quit))))

      ; births and deaths
      (when (not done?)
        (set! births
          (ceiling (* (randint 5 0) (+ (* 20 acres) stores) (/ pop) 1/100)))
        (set! deaths (- pop (quotient feed 20)))
        (set! total-deaths (+ total-deaths deaths))
        (set! pcnt-starved
          (/ (+ (* (- year 1) pcnt-starved)
                (floor (* deaths 100 (/ pop))))
             year))
        (set! pop (+ pop births (- deaths)))
        (when (< (* 0.45 pop) deaths) (set! done? #t) (impeach deaths)))

      ; loop for next year
      (when (not done?) (set! plague? (< (rand) 0.15)) (loop (+ year 1))))))

Hopefully the comments make clear what is happening. The program begins by printing a welcome message and initializing a bunch of global variables, then enters a loop with one year per iteration. The done? variable is set whenever there needs to be an early termination, due to high starvation or invalid data entry, and each of the pieces begins by checking done? so the remaining work is skipped and the loop terminates if the game is over.

The first block in the main loop checks plague? and reports the current status:

(define (report year deaths births plague? pop acres yield rats stores)
  (for-each display `(
    #\newline #\newline
    "HAMURABI:  I BEG TO REPORT TO YOU," #\newline
    "IN YEAR " ,year ", " ,deaths " PEOPLE STARVED, "
    ,births " CAME TO THE CITY," #\newline))
  (when plague?
    (display "A HORRIBLE PLAGUE STRUCK!  HALF THE PEOPLE DIED.") (newline))
  (for-each display `(
    "POPULATION IS NOW " ,pop #\newline
    "THE CITY OWNS " ,acres " ACRES." #\newline
    "YOU HARVESTED " ,yield " BUSHELS PER ACRE." #\newline
    "RATS ATE " ,rats " BUSHELS." #\newline
    "YOU NOW HAVE " ,stores " BUSHELS IN STORE." #\newline #\newline)))

The second block checks for normal termination at the end of ten years. Here is terminate, which writes the termination header, followed by the series of four messages that terminate might write, followed by the so-long exit message (note that #\bel is specific to Chez Scheme, and other Scheme systems may use a different mnemonic):

(define (terminate pcnt-starved total-deaths acres pop)
  (let ((land (quotient acres pop)))
    (for-each display `(
      "IN YOUR 10-YEAR TERM OF OFFICE, "
      ,(inexact->exact (round pcnt-starved))
      " PERCENT OF THE" #\newline
      "POPULATION STARVED PER YEAR ON AVERAGE, I.E., A TOTAL OF"
      #\newline ,total-deaths " PEOPLE DIED!!" #\newline
      "YOU STARTED WITH 10 ACRES PER PERSON AND ENDED WITH "
      ,land " ACRES PER PERSON." #\newline #\newline))
    (cond ((or (< 33 pcnt-starved) (< land 7)) (fink))
          ((or (< 10 pcnt-starved) (< land 9)) (nero))
          ((or (< 3 pcnt-starved) (< land 10)) (not-bad pop))
          (else (fantastic)))))

(define (fink)
  (display "DUE TO THIS EXTREME MISMANAGEMENT YOU HAVE NOT ONLY") (newline)
  (display "BEEN IMPEACHED AND THROWN OUT OF OFFICE BUT YOU HAVE") (newline)
  (display "ALSO BEEN DECLARED 'NATIONAL FINK' !!") (newline) (so-long))

(define (nero)
  (for-each display `(
    "YOUR HEAVY-HANDED PERFORMANCE SMACKS OF NERO AND IVAN IV." #\newline
    "THE PEOPLE (REMAINING) FIND YOU AN UNPLEASANT RULER, AND," #\newline
    "FRANKLY, HATE YOUR GUTS!")) (so-long))

(define (not-bad pop)
  (for-each display `(
    "YOUR PERFORMANCE COULD HAVE BEEN SOMEWHAT BETTER, BUT"
    #\newline "REALLY WASN'T TOO BAD AT ALL. "
    ,(randint (floor (* pop 4/5)) 0) " PEOPLE WOULD" #\newline
    "DEARLY LIKE TO SEE YOU ASSASSINATED BUT WE ALL HAVE OUR "
    "TRIVIAL PROBLEMS." #\newline)) (so-long))

(define (fantastic)
  (for-each display `(
    "A FANTASTIC PEFORMANCE!!!  CHARLEMAGNE, DISRAELI, AND" #\newline
    "JEFFERSON COMBINED COULD NOT HAVE DONE BETTER" #\newline)) (so-long))

(define (so-long)
  (do ((n 1 (+ n 1))) ((< 10 n)) (display #\bel))
  (newline) (display "SO LONG FOR NOW.") (newline) (newline))

The third block asks the player to buy or sell land. First the price of land is randomly chosen, then the player is asked to buy land, and if he declines, he is asked to sell land. When buying, there is a check to be sure the player has sufficient grain to make the purchase, and when selling, there is a check to be sure the player owns the requested number of acres. If land is exchanged, both the number of acres owned and the number of bushels of grain in the silos are updated. Note that get-buy-sell returns #f to terminate the game if the player enters a negative number of acres:

(define (get-price)
  (let ((price (+ (randint 10) 17)))
    (for-each display `(
      "LAND IS TRADING AT " ,price " BUSHELS PER ACRE." #\newline))
    price))

(define (get-buy-sell price stores acres)
  (let ((q (get-buy price stores acres)))
    (cond ((not q) #f) ((positive? q) q)
          (else (let ((q (get-sell acres)))
                  (if q (- q) #f))))))

(define (get-buy price stores acres)
  (display "HOW MANY ACRES DO YOU WISH TO BUY? ")
  (let ((q (read)))
    (cond ((negative? q) #f)
          ((< (* price q) stores) q)
          (else (no-stores stores) (get-buy price stores acres)))))

(define (get-sell acres)
  (display "HOW MANY ACRES DO YOU WISH TO SELL? ")
  (let ((q (read)))
    (cond ((negative? q) #f) ((< q acres) q)
          (else (no-acres acres) (get-sell acres)))))

The fourth block asks the player how much to feed the people and updates the number of bushels of grain in the silos. The only calculation is whether or not there is sufficient grain on hand to meet the request:

(define (get-feed stores)
  (newline) (display "HOW MANY BUSHELS DO YOU WISH TO FEED YOUR PEOPLE? ")
  (let ((q (read)))
    (cond ((negative? q) #f)
          ((< stores q) (no-stores stores) (get-feed stores))
          (else q))))

The fifth block calculates the harvest and sets the ending number of bushels of grain in the silos. Each person can farm ten acres, and each acre requires half a bushel for seed; inputs are checked for validity against both constraints. The yield per acre planted is calculated randomly, as is the amount of grain eaten by rats. Note that stores is updated twice, once to reduce grain when it is planted, and once to increase grain when it is harvested (less the amount eaten by rats):

(define (get-plant acres stores pop)
  (newline) (display "HOW MANY ACRES DO YOU WISH TO PLANT WITH SEED? ")
  (let ((q (read)))
    (cond ((negative? q) #f) ((zero? q) q)
          ((< acres q) (no-acres acres) (get-plant acres stores pop))
          ((< stores (quotient q 2))
            (no-stores stores) (get-plant acres stores pop))
          ((< (* pop 10) q) (no-people pop) (get-plant acres stores pop))
          (else q))))

The previous three blocks all call the functions shown below to inform the user he has violated some constraint and ask him to re-enter a valid amount; also shown below is the function that terminates the program for invalid input:

(define (no-stores stores)
  (for-each display `(
    "HAMURABI:  THINK AGAIN.  YOU HAVE ONLY " ,stores
    " BUSHELS OF GRAIN.  NOW THEN," #\newline)))

(define (no-acres acres)
  (for-each display `(
    "HAMURABI:  THINK AGAIN.  YOU ONLY OWN "
    ,acres " ACRES.  NOW THEN," #\newline)))

(define (no-people pop)
  (for-each display `(
    "BUT YOU HAVE ONLY " ,pop
    " PEOPLE TO TEND THE FIELDS.  NOW THEN," #\newline)))

(define (quit)
  (newline) (display "HAMURABI:  I CANNOT DO WHAT YOU WISH.") (newline)
  (display "GET YOURSELF ANOTHER STEWARD!!!!!") (newline) (so-long))

The sixth block calculates the ending population. Births are calculated randomly, deaths are calculated based on a requirement of twenty bushels of grain to feed one person for a year. The total number of deaths due to starvation and the percentage starved are accumulated for reporting at the end of the game; note that deaths due to plague don’t count against the steward. Finally, the steward is impeached if too many people starve in any one year; the earlier “national fink” message is reused for this purpose. Here’s the message that reports the impeachment:

(define (impeach deaths)
  (for-each display `(
    #\newline "YOU STARVED " ,deaths " PEOPLE IN ONE YEAR!!!" #\newline))
  (fink))

Finally, the seventh block calculates a fifteen percent probability of plague and loops for the next year.

A complete sample game is given on the next page. You can see the code assembled at http://programmingpraxis.codepad.org/kx22J8TE.

Pages: 1 2 3 4

15 Responses to “HAMURABI.BAS”

  1. TravisH82 said

    Here’s my rendition in Java. I’m really new to it so your thoughts on how I can improve my code are appreciated

  2. TravisH82 said

    oh… Sorry.. here’s the link: http://pastebin.com/j32JaSEw

  3. alexander said

    One more rendition, in lua: http://codepad.org/pc8FSfnQ

    It seems that average size of population in optimal game should be somewhere between 380 and 390 (if we remove restriction on the game duration).

  4. alexander said

    Hm… How INT and RND are work in the basic? Is INT(5*RND(1)) uniformy distributed in {0, 1, .., 4}?

  5. TravisH82 said

    Wish you were allowed to edit your posts here.. or maybe you can and I don’t know how.. anyways. I think I figured out how to embed the code in the comments here…

  6. Rörd said

    > Hm… How INT and RND are work in the basic? Is INT(5*RND(1)) uniformy distributed in {0, 1, .., 4}?

    IIRC yes. INT will truncate a floating point number (cut off everything behind the point), and RND returns a floating point number between 0 (including) and 1 (excluding).
    (The argument to RND will be used as new seed, when negative; create a new seed, when zero; and use the existing seed, when positive.)

  7. Rörd said

    So here’s my reimplementation in Common Lisp: http://lisp.pastebin.com/wnUkj52V

    I started with using tagbody/go for the control flow, but I had replaced all uses of these before I arrived at a working version.

  8. Rörd said

    Sorry, there was a problem with the paste. The URL is now: http://lisp.pastebin.com/r1gL4zYT

  9. John said

    I wrote a version in Factor and blogged about it:

    http://re-factor.blogspot.com/2010/08/hamurabi.html

    The implementation can be seen here:

    http://paste.factorcode.org/paste?id=1833

  10. […] HAMURABI.BAS « Programming Praxis Your task is to reimplement HAMURABI.BAS in a more modern computer language. Don’t peek at the solution unless you want to deprive yourself of the sheer joy of working out the spaghetti code and figuring out what the variables really stand for. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below. (tags: dev compsci programming todo) […]

  11. gold said

    http://wiki.tcl.tk/26775,
    Loaded an etcl version of Hamurabi with
    either a small console demo version or
    a larger etcl canvas version.
    I’m calling it Game kingdom of Strategy
    gold

  12. Keith said

    Wow, this is an old thread. I was thinking about the early days of home computing, and remembered playing Hamurabi in school and a web search brought me here. TravisH82’s java code ( http://pastebin.com/j32JaSEw) is not bad, but never resets death back to zero. I noticed that if 29 people starved, even if I fed everybody in the subsequent turn I still had 29 deaths again.

    This corrects the problem:

    if (population > fullPeople) {
      deaths = population - fullPeople;
      if (deaths > .45 * population)
        epicFail(1);
      percentDied = ((year - 1) * percentDied + deaths * 100 / population) / year;
      population = fullPeople;
      totalDeaths += deaths;
    } else { deaths = 0;
    }
    

    There also seems to be a problem with calculating sowable land in turn 1. I can start with 2800 bushels, buy 10 acres 18 each, give 2000 bushels in food and still sow 1010 acres when I ought to have been limited to 620.

  13. Keith said

    OK, I understand now… each bushel suffices to sow two acres of land.

  14. Oleg said

    I worked it out on Visual BaSICK. Now I only need to translate ancient english words to civilised languade. All that MORISTOWNs, GOVERNINGs, ACRESsss…

Leave a comment