š³ Evergreen noteworthy

# Porting BASIC Space Mines Game to Go

## planted on May 5, 2017 in: Programming, BASIC, Quick BASIC, Go and Retro.~4,393 words, about a 22 min read.

I recently spent a little time going down memory lane and reminiscing about my first experiences with programming. I even wrote a short article of the journey during which I wished BASIC a happy 53rd birthday.

While writing that article I also sat down and ported a BASIC game called Space Mines to Go. Now I am far from an expert in Golang, and would actually call myself a beginner. However, I found it remarkably easy to port the game and wanted to document my thoughts while doing so ā so lets talk about that.

Space Mines is a command line strategy/management game. It was published in 1982 by Usborne as BASIC source code within the book Computer spacegames. In the same year it was also released on tape for the ZX Spectrum 16K, for more information on that release click here.

The aim of the game is to survive 10 years in office managing a mining colony in space. While this sounds simple enough the game is somewhat weighted against you with the food mechanic being the worst. It is however possible to win, you just need to pray to the random number gods that they provide numbers in your favour.

The BASIC source code begins by defining some initial environment variables: `L` is the number of mines, `P` the number of people, `M` the amount of money, `FP` the price of food, `CE` the amount of ore produced per mine, `C` the amount of ore in storage, `S` the satisfaction factor, `Y` the year, `LP` the buying/selling price mines and `CP` the selling price for ore.

``10 LET L=INT(RND*3+5)20 LET P=INT(RND*60+40)30 LET M=INT(RND*50+10)*P40 LET FP=INT(RND*40+80)50 LET CE=INT(RND*40+80)60 LET C=070 LET S=180 LET Y=190 LET LP=INT(RND*2000+2000)100 LET CP=INT(RND*12+7)110 CLS``

For some reason that the `FP` variable is used nowhere else in the game, I don't personally know the history behind this or whether the version available on tape for the ZX Spectrum was different however the variables purpose is referenced in the book, it is just never used.

To generate the random numbers in Go I chose to use the math/rand package as it is simple to use so long as you remember to seed it before you request a random number. To save a lot of lines of code I created a `random` function that could produce a random number within a range:

``func random(min, max int) int {    rand.Seed(time.Now().UnixNano())    return rand.Intn(max - min) + min}``

As for the variables, they lend themselves nicely to being part of a Colony `struct` resulting in the following Go code:

``type Colony struct {    numMines int            // L    numPeople int           // P    money int               // M    food int    foodPrice int           // FP - not used?    oreProduction int       // CE    oreStorage int          // C    year int                // Y    satisfaction float32    // S    minePrice int           // LP    orePrice int            // CP    failed bool}func initColony() *Colony {    c := Colony{}    c.numMines = random(3,6)    c.numPeople = random(40, 60)    c.money = random(10, 50) * c.numPeople    c.foodPrice = random(40,80)    c.oreProduction = random(40,80)        c.oreStorage = 0    c.year = 1;    c.satisfaction = 1        c.rollPriceDice()        c.failed = false    return &c;}func (c *Colony) rollPriceDice() {    c.minePrice = random(2000, 4000)    c.orePrice = random(7, 12)}``

As you can see from the above code, a new Colony object is created through use of the `initColony` constructor. The BASIC source uses `GOTO` at the end of each year to return to line 90, therefore the mine and ore selling prices are broken out into the `rollPriceDice` function because they are re-rolled each year.

Once all the variables have been set the BASIC source then proceeds to print the current state of affairs in the colony:

``120 PRINT "YEAR";Y130 PRINT140 PRINT "THERE ARE ";P;" PEOPLE IN THE COLONY"150 PRINT "YOU HAVE ";L;" MINES, AND \$";M160 PRINT "SATISFACTION FACTOR ";S170 PRINT180 PRINT "YOUR MINES PRODUCED ";CE;" TONS EACH"190 LET C=C+CE*L200 PRINT "ORE IN STORE=";C;" TONS"``

In Go I wrote the above as a `displayColonyStats` method attached to the Colony `struct`, the big difference being that I decided not to use all caps:

``func (c *Colony) displayColonyStats() {    fmt.Println("Year", c.year)    fmt.Println("There are", c.numPeople, "people in the colony")    fmt.Println("You have", c.numMines, "mines and \$", c.money)    fmt.Println("Satisfaction Factor ", c.satisfaction)    fmt.Println("")    fmt.Println("Your mines produced ", c.oreProduction, "tons each")        c.oreStorage += c.oreProduction * c.numMines        fmt.Println("Ore in store:", c.oreStorage, "tons")}``

Once the colony status has been output the BASIC source enters a "selling mode" by first printing the ore and mine selling prices before asking how much of each you would like to sell:

``210 PRINT "SELLING"220 PRINT "ORE SELLING PRICE=";CP230 PRINT "MINE SELLING PRICE=";LP;"/MINE"240 PRINT "HOW MUCH ORE TO SELL?"250 INPUT CS260 IF CS<0 OR CS>C THEN GOTO 240270 LET C=C-CS280 LET M=M+CS*CP290 PRINT "HOW MANY MINES TO SELL?"300 INPUT LS310 IF LS<0 OR LS>L THEN GOTO 290320 LET L=L-LS330 LET M=M+LS*LP``

Being a beginner at Go it took me a few attempts at getting the desired functionality in Go as `INPUT` provides in BASIC. I didn't want to include any additional packages if possible and turned to the `Scanf` method of the fmt package already in use. This resulted in a `askForIntInput` function that loops until a valid user input is provided. I then used this in two methods attached to the Colony `struct`: `oreSale` and `mineSale`:

``func askForIntInput(s string) int {    var output int    for {        fmt.Printf("%s", s)        _, err := fmt.Scanf("%d\n",&output)                if err != nil {            fmt.Println("That input was invalid")            continue        } else {            break        }    }    return output}func (c *Colony) oreSale() {    for {        oreToSell := askForIntInput("How much ore to sell? ")        if oreToSell >= 0 && oreToSell <= c.oreStorage{            c.oreStorage -= oreToSell            c.money += oreToSell * c.orePrice        break        }    }}func (c *Colony) mineSale() {    for {        minesToSell := askForIntInput("How many mines to sell? ")        if minesToSell >= 0 && minesToSell <= c.numMines{            c.numMines -= minesToSell            c.money += minesToSell * c.minePrice            break        }    }}``

It is quite a few more lines of code to replicate the same functionality in Go as is provided in BASIC, but both sources do exactly the same thing and I am sure someone who knows Go better than me could probably get the line count down if they where feeling like a little code-golf.

I output the market prices within the `main` function of my Go port, which we will get to in a moment. First lets go back to the BASIC source where it is now outputting the players amount of money and switching to buy mode:

``340 PRINT350 PRINT "YOU HAVE \$";M360 PRINT370 PRINT "BUYING"380 PRINT "HOW MUCH TO SPEND ON FOOD? (APPR. \$100 EA.)"390 INPUT FB400 IF FB<0 OR FB>M THEN GOTO 380410 LET M=M-FB420 IF FB/P>120 THEN LET S=S+.1430 IF FB/P<80 THEN LET S=S-.2440 PRINT "HOW MANY MORE MINES TO BUY?"450 INPUT LB460 IF LB<0 OR LB*LP>M THEN GOTO 440470 LET L=L+LB480 LET M=M-LB*LP``

Most notable in the original BASIC source code here is the hardcoded output of "(APPR. \$100 EA.)" and in a section where you would expect to see the `FP` variable used, it is missing. This may be because of limited space being made available in the book to this particular game and another longer version may be out there. It is also possible that the ZX Spectrum version makes use of the `FP` variable ā I have yet to do further research on either prospect.

The core difficulty of this game comes from lines 420 and 430. For a population of 40 you need to spend at least \$4000 on food to keep the satisfaction increasing otherwise by your third year you will have a satisfaction below 0.6 and it will be game over. However with a high satisfaction you end up with your population increasing and so you're always inevitably spending the majority of your money on food.

The above fifteen or so lines translates to the following in Go:

``func (c *Colony) foodBuy() {    for {        foodToBuy := askForIntInput("How much to spend on food? (Appr. \$100 EA.) ")        if foodToBuy >= 0 && foodToBuy <= c.money{            c.food += foodToBuy            c.money -= foodToBuy                        if foodToBuy / c.numPeople > 120 {                c.satisfaction+=.1            }                        if foodToBuy / c.numPeople < 80 {                c.satisfaction-=.2            }            break        }else{            fmt.Println("You don't have enough money to afford that amount of food.")        }    }}func (c *Colony) mineBuy() {    for {        minesToBuy := askForIntInput("How many more mines to buy? ")        if minesToBuy >= 0 && (minesToBuy * c.minePrice) <= c.money{            c.numMines += minesToBuy            c.money -= minesToBuy * c.minePrice            break        }    }}``

I think at the time of porting this game from BASIC to Go I got confused by the variables being used and though that the game tracked the amount of food that you had in store thus the `c.food += foodToBuy`. This is not actually the case as you can see from the BASIC code above but I left it in because I aim to to implement that functionality in the future.

Now the game has taken the users input it begins the final phase for the in-game year ā working out if there is a game over:

``490 IF S<.6 THEN GOTO 660500 IF S>1.1 THEN LET CE=CE+INT(RND*20+1)510 IF S<.9 THEN LET CE=CE-INT(RND*20+1)520 IF L/L<10 THEN GOTO 680530 IF S>1.1 THEN LET P=P+INT(RND*10+1)540 IF S<.9 THEN LET P=P-INT(RND*10+1)550 IF P<30 THEN GOTO 700560 IF RND>.01 THEN GOTO 590570 PRINT "RADIOACTIVE LEAK....MANY DIE"580 LET P=INT(P/2)590 IF CE<150 THEN GOTO 620600 PRINT "MARKET GLUT - PRICE DROPS"610 LET CE=INT(CE/2)620 LET Y=Y+1630 IF Y<11 THEN GOTO 90640 PRINT "YOU SURVIVED YOUR TERM OF OFFICE"650 STOP660 PRINT "THE PEOPLE REVOLTED"670 STOP680 PRINT "YOU'VE OVERWORKED EVERYONE"690 STOP700 PRINT "NOT ENOUGH PEOPLE LEFT"710 STOP``

As mentioned before, if the satisfaction reaches less than .6 then the people will revolt and its game over; however there are two other end games that are just as likely: not having enough people left with a population of less than 30 and having over worked everyone with less than 10 people per mine.

When satisfaction is high (> 1.1) you get an increase to both your population and productivity however when its low (<.9) you get a decrease to both. There is also a small chance that half your population will be wiped out by a radioactive leak and without a high satisfaction, selling off most your mines and extremely good luck with the next migration of people this almost certainly means game over.

In my Go port, this was all rolled into the `main` function, with the addition of a `randomFloat` helper function because I thought I might need it in the future:

``func randomFloat() float64 {    rand.Seed(time.Now().UnixNano())    return rand.Float64()}func main(){    c := initColony()        for c.year <= 10 && c.failed == false {        c.displayColonyStats()                // Selling        fmt.Println("Selling:")        fmt.Println("Ore selling price: \$", c.orePrice, "/ton")        fmt.Println("Mine selling price: \$", c.minePrice, "/mine")                c.oreSale()        c.mineSale()                // Buying        fmt.Println("")        fmt.Println("You have \$", c.money)        fmt.Println("")        fmt.Println("Buying")                c.foodBuy()        c.mineBuy()                // If there are less than 10 people per mine then game over        if c.numPeople / c.numMines < 10 {            c.failed = true            fmt.Println("You've overworked everyone, Game Over!")            break        }                // If satisfaction is high, more people arrive        if c.satisfaction > 1.1 {            c.numPeople += random(1,10)        }                // People leave if satisfaction is low        if c.satisfaction < 0.9 {            c.numPeople -= random(1,10)        }                // If the satisfaction is too low then game over        if c.satisfaction < 0.6 {            c.failed = true            fmt.Println("The people revolted, Game Over!")            break        }                // If there are less than 30 people in total then game over        if c.numPeople < 30 {            c.failed = true            fmt.Println("Not enough people left, Game Over!")            break        }                // Introduce a small chance that half the population gets killed        if randomFloat() < 0.1 {            fmt.Println("RADIOACTIVE LEAK....MANY DIE!")            c.numPeople /= 2        }                // If the amount produced per mine is very high, ore price is halved        if c.oreProduction > 150 {            fmt.Println("Market Glut - Price Drops!")            c.foodPrice /= 2        }                // Player has survived another year        c.rollPriceDice()        c.year++        fmt.Println("")    }        if c.failed == false {        fmt.Println("You survived your term of office")    }}``

The BASIC version comes in at 71 lines in total, with the Go port having 144 more lines; however that also includes formatting and comments so without actually stripping those out I am sure both versions are close regarding LOC.

For some reason this game has stuck in my mind, it could be because the book that I found it in has very pretty illustrations that captured my imagination the first time I typed this out into my Toshiba T3100e all those years ago.

I have in the past seen other peoples ports to languages such as C++ where they have extended it with additional end games as well as refining the satisfaction and food mechanism. I have made my port available on github here and would very much appreciate a fork if you would like to add your own spin on this text based game from the 80s.

Page History

This page was first added to the repository on March 21, 2021 in commit 83e1321a and has since been amended 6 times. View the source on GitHub.