Rewriting and Refactoring Hamurabi.bas in Javascript

Sometimes, I have good ideas, and other times, I have stupid ideas. This might be one of the stupid ones. I wanted to play the Hamurabi game, and found some on the web, but they were so slow. Maybe it could be recoded into Javascript… I thought.

So, here’s my work so far. The original code is at http://www.moorecad.com/classicbasic/basic/creative/hamurabi.bas.

I started by changing the single-letter variable to something more meaningful.

Then, I added some labels, to replace line numbers. That helped explain some of the program structure.

Then, I started to break out the code into separate functions. This is kind of tricky.

My goal isn’t just to break out the functions, but to make some of the functions stateless, so the entire game can be treated as a series of states, with functions that produce transformed states. More on this below the code.

This code is in transition, and it doesn’t work.

//10 PRINT TAB(32);"HAMURABI"
//20 PRINT TAB(15);"CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY"
//30 PRINT:PRINT:PRINT
//80 PRINT "TRY YOUR HAND AT GOVERNING ANCIENT SUMERIA"
//90 PRINT "FOR A TEN-YEAR TERM OF OFFICE.":PRINT

//95 starve1=0: population1=0
//100 year=0: population=95: stores=2800: harvest=3000: eaten=harvest-stores
//110 yield=3: acres=harvest/yield: immigrant=5: plague=1
//210 starve=0

state = {
    year: 1,
    population: 95,
    stores: 3800,
    harvest: 3000,
    eaten: 200,
    yield: 3,
    acres: 1000,
    immigrant: 5,
    plague: false,
    starve: 0,
    totalStarved: 0,
    landprice: 17,
    feed: 0,
    performance: 0
}

/*
start_report:
215 PRINT:PRINT:PRINT "HAMURABI:  I BEG TO REPORT TO YOU,": year=year+1
217 PRINT "IN YEAR";year;",";starve;"PEOPLE STARVED,";immigrant;"CAME TO THE CITY,"
218 population=population+immigrant
227 IF plague>0 THEN 230
228 population=INT(population/2)
229 PRINT "A HORRIBLE PLAGUE STRUCK!  HALF THE PEOPLE DIED."
230 PRINT "POPULATION IS NOW";population
232 PRINT "THE CITY NOW OWNS ";acres;"ACRES."
235 PRINT "YOU HARVESTED";yield;"BUSHELS PER ACRE."
250 PRINT "THE RATS ATE";eaten;"BUSHELS."
260 PRINT "YOU NOW HAVE ";stores;"BUSHELS IN STORE.": PRINT
270 IF year=11 THEN 860 // end_of_game_summary
*/
function update_report(state) {
}
<code>
function init_first_turn(state) {
    state.landprice = 17 + int(random()*10);
}

function play_turn(state) {
    newstate = {};
    newstate.year = state.year + 1;
    stores = state.stores;

    //532 REM *** LET'S HAVE SOME BABIES
    //533 immigrant=INT(C*(20*acres+stores)/population/100+1)
    newstate.immigrant = int(random() * (20 * state.acres + state.stores) / state.population / 100 + 1);
    newstate.population = state.population + newstate.immigrant;
    //541 REM *** HORROS, A 15% CHANCE OF PLAGUE
    //542 plague=INT(10*(2*RND(1)-.3))
    newstate.plague = int(10 * (2*random() - 0.3));
    if (newstate.plague) {
        newstate.population = int(newstate.population/2);
    }
    // for the next stores, we start by removing grain that's planted
    //510 stores=stores-INT(plant/2)
    stores -= int(state.plant / 2);

    // then we calculate a harvest
    //511 GOSUB random_c
    //512 REM *** A BOUNTIFUL HARVEST!
    //515 yield=C: harvest=plant*yield: eaten=0
    newstate.yield = random();
    harvest = state.plant * newstate.yield;

    // see if rats eat into the stores
    //521 GOSUB random_c
    //522 IF INT(C/2)<>C/2 THEN 530 // rats eat nothing
    rats = random();
    if (int(c/2)!=c/2) {
        //523 REM *** RATS ARE RUNNING WILD!!
        //525 eaten=INT(stores/C)
        eaten = int(stores/rats);
    }
    // net stored grain
    // 530 stores=stores-eaten+harvest
    newstate.stores = stores - eaten + harvest;
    newstate.harvest = harvest;
    newstate.eaten = eaten;

    // let's see how many people immigrated - note that it used
    // to be births.
    // based on the size of the country, grain stored, and existing population
    //531 GOSUB random_c
    //532 REM *** LET'S HAVE SOME BABIES
    //533 immigrant=INT(C*(20*acres+stores)/population/100+1)
    newstate.immigrant = int(random()*state.acres + state.stores)/state.population/100 + 1);

    // Code goes into spaghetti here.
    //539 REM *** HOW MANY PEOPLE HAD FULL TUMMIES?
    //540 feedingcapacity=INT(feedbushels/20)
    capacity = int(state.feed/20);
    //550 IF population<feedingcapacity THEN 210
    if (state.population < capacity) {
        //551 REM *** STARVE ENOUGH FOR IMPEACHMENT?
        starve = state.population - capacity;
        // we ignore the checks and will do them in another function
        //552 starve=population-feedingcapacity: IF starve>.45*population THEN 560
        //553 population1=((year-1)*population1+starve*100/population)/year
        //555 population=feedingcapacity: starve1=starve1+starve
        newstate.population = capacity;
        newstate.totalStarved = state.totalStarved + starve;
        newstate.performance = ((state.year)*state.performance + starve*100/state.population)/state.year;
        newstate.starve = starve;
    }

    newstate.landprice = 17 + int(random()*10);

    return newstate;
}

function check_for_impeachement(state) {
    if (state.starve > 0.45 * state.population) return true;
    return false;
}

function random() { return int(Math.random()*5) + 1; }

/* buy and sell land */
/*
310 C=INT(10*RND(1)): landprice=C+17
312 PRINT "LAND IS TRADING AT";landprice;"BUSHELS PER ACRE."

buy_land:
320 PRINT "HOW MANY ACRES DO YOU WISH TO BUY";
321 INPUT acquire: IF acquire<0 THEN 850
322 IF landprice*acquire<=stores THEN 330
323 GOSUB 710
324 GOTO buy_land

330 IF acquire=0 THEN 340
331 acres=acres+acquire: stores=stores-landprice*acquire: C=0
334 GOTO 400

sell_land:
340 PRINT "HOW MANY ACRES DO YOU WISH TO SELL";
341 INPUT sell: IF sell<0 THEN 850
342 IF sell<acres THEN 350
343 GOSUB 720
344 GOTO sell_land

350 acres=acres-sell: stores=stores+landprice*sell: C=0
400 PRINT
*/
/** 
 *  Buy or sell land. Change is the change in acres. 
 *  Negative values mean sell land to get wheat.
 *  Positive values mean buy land in exchange for wheat.
 *  Todo - add exceptions.
 */
function buy_sell_land(state, change) {
    p = state.landprice;
    newstate = clone(state);
    if (change == 0) return state;
    if (change < 0) {
        if (-change > state.acres) {
            // error - not enough land
            return state;
        }
    } else if ( change > 0 ) {
        if (p * change > state.stores) {
            // error - not enough wheat
            return state;
        }
    }
    newstate.stores = state.stores - change * p;
    newstate.acres = state.acres + change;
    return newstate;
}

/* feed the people */
/*
feed:
410 PRINT "HOW MANY BUSHELS DO YOU WISH TO FEED YOUR PEOPLE";
411 INPUT feedbushels
412 IF feedbushels<0 THEN 850
418 REM *** TRYING TO USE MORE GRAIN THAN IS IN SILOS?
420 IF feedbushels<=stores THEN 430
421 GOSUB 710
422 GOTO feed
430 stores=stores-feedbushels: C=1: PRINT

    Feed people. Change is the bushels to feed.
    Sets the feed property that's used to calculate the next turn.

*/
function feed_people(state, change) {
    newstate = clone(state);
    if (change > state.stores) {
        // error - can't feed this many people
        return state;
    }
    newstate.feed = change;
    newstate.stores -= change;
}

/* plant seed */
/*
plant:
440 PRINT "HOW MANY ACRES DO YOU WISH TO PLANT WITH SEED";
441 INPUT plant: IF plant=0 THEN 511
442 IF plant<0 THEN 850
444 REM *** TRYING TO PLANT MORE ACRES THAN YOU OWN?
445 IF plant<=acres THEN 450
446 GOSUB 720
447 GOTO plant

449 REM *** ENOUGH GRAIN FOR SEED?
450 IF INT(plant/2)<=stores THEN 455
452 GOSUB 710
453 GOTO plant

454 REM *** ENOUGH PEOPLE TO TEND THE CROPS?
455 IF plant<10*population THEN 510
460 PRINT "BUT YOU HAVE ONLY";population;"PEOPLE TO TEND THE FIELDS!  NOW THEN,"
470 GOTO plant

    Plant grain. Change is the number of acres to plant.
*/
function plant_grain(state, change) {
    newstate = clone(state);
    if (change > state.acres) {
        // error - not enough land
        return state;
    }
    if (change/2 < state.stores) {
        // error - not enough grain
        return state;
    }
    if (change < 10*state.population) {
        // error - not enough people to plant grain
        return state;
    }
    newstate.plant = change;
    return;
}


/* recalculate state for next turn */


starvation_error:
560 PRINT: PRINT "YOU STARVED";starve;"PEOPLE IN ONE YEAR!!!"
565 PRINT "DUE TO THIS EXTREME MISMANAGEMENT YOU HAVE NOT ONLY"
566 PRINT "BEEN IMPEACHED AND THROWN OUT OF OFFICE BUT YOU HAVE"
567 PRINT "ALSO BEEN DECLARED NATIONAL FINK!!!!": GOTO 990

grain_error:
710 PRINT "HAMURABI:  THINK AGAIN.  YOU HAVE ONLY"
711 PRINT stores;"BUSHELS OF GRAIN.  NOW THEN,"
712 RETURN

land_error:
720 PRINT "HAMURABI:  THINK AGAIN.  YOU OWN ONLY";A;"ACRES.  NOW THEN,"
730 RETURN

random_c:
800 C=INT(RND(1)*5)+1
801 RETURN

other_error:
850 PRINT: PRINT "HAMURABI:  I CANNOT DO WHAT YOU WISH."
855 PRINT "GET YOURSELF ANOTHER STEWARD!!!!!"
857 GOTO 990

/* end of game summary */

end_game:
860 PRINT "IN YOUR 10-YEAR TERM OF OFFICE,";population1;"PERCENT OF THE"
862 PRINT "POPULATION STARVED PER YEAR ON THE AVERAGE, I.E. A TOTAL OF"
865 PRINT starve1;"PEOPLE DIED!!": L=acres/population
870 PRINT "YOU STARTED WITH 10 ACRES PER PERSON AND ENDED WITH"
875 PRINT L;"ACRES PER PERSON.": PRINT

880 IF population1>33 THEN 565
885 IF L<7 THEN 565
890 IF population1>10 THEN 940
892 IF L<9 THEN 940
895 IF population1>3 THEN 960
896 IF L<10 THEN 960

900 PRINT "A FANTASTIC PERFORMANCE!!!  CHARLEMANGE, DISRAELI, AND"
905 PRINT "JEFFERSON COMBINED COULD NOT HAVE DONE BETTER!":GOTO 990

940 PRINT "YOUR HEAVY-HANDED PERFORMANCE SMACKS OF NERO AND IVAN IV."
945 PRINT "THE PEOPLE (REMIANING) FIND YOU AN UNPLEASANT RULER, AND,"
950 PRINT "FRANKLY, HATE YOUR GUTS!!":GOTO 990

960 PRINT "YOUR PERFORMANCE COULD HAVE BEEN SOMEWHAT BETTER, BUT"
965 PRINT "REALLY WASN'T TOO BAD AT ALL. ";INT(population*.8*RND(1));"PEOPLE"
970 PRINT "WOULD DEARLY LIKE TO SEE YOU ASSASSINATED BUT WE ALL HAVE OUR"
975 PRINT "TRIVIAL PROBLEMS."

990 PRINT: FOR N=1 TO 10: PRINT CHR$(7);: NEXT N
995 PRINT "SO LONG FOR NOW.": PRINT
999 END

I’m having a little trouble wrapping my head around making the code stateless. By stateless, I don’t mean that there’s no state at all. Rather, the state is kept external to the functions that contain the game logic. The game is modeled as a series of turns, and each turn contains a state – a snapshot of the game in progress. Each state contains both the inputs and outputs for each turn; the inputs are the player’s decisions, and the outputs are the results.

When the player manipulates the UI, but before the turn is played, the program must validate the inputs, and allow only valid inputs. There are functions to check the inputs against the game state, and these functions are also stateless. To perform these interactive validations, we need to create a “scratch state” or a copy of the previous turn, and then apply transformations against that copy. (There’s no code to do this above.) Once all these interactive changes to the scratch state are made, then we run a function that “plays” the turn. The state returned is the turn.

Next: Rewriting and Refactoring Hamurabi.bas in Javascript Part 2

One thought on “Rewriting and Refactoring Hamurabi.bas in Javascript”

Leave a Reply