Santa Paravia en Fiumaccio in C#
Update: You can now experience the game on this page thanks to a WebAssembly conversion I did.
Entering the rabbit hole
Recently, I got into a rabbit hole of old city planning games. The very first I found was The Sumerian Game. Created by Mabel Addis in 1964, it was one of the earliest text-based strategy video games of land and resource management.
Other games in the same vain are Hamurabi (1968), Dukedom (1976) and Dynasty (1978).
Nowadays, text-based games see some kind of renaissance, thanks to Warsim or Roots of Harmony
In this article, I will focus on Santa Paravia en Fiumaccio, a game set in the 15th century in italy. This game was created in 1978 by George Blank, and published in the December 1978 issue of SoftSide magazine for the TRS-80.
Wikipedia says this about the game:
Based loosely on the text game Hamurabi, Santa Paravia and Fiumaccio was an early god game. It combined 'guns and butter' economic tradeoffs with graphic development of a kingdom with buildings being constructed and shown on the screen as well as character development, shown as progressive promotions from baron to king.
The goal of the game is to progress from Lady (or Sir) to H.R.H Queen (or H.R.H King), meanwhile defending attackers and distributing grain according to need and availablilty. You can (and should) buy additional land, grow grain and control taxes.
A fellow called Thomas Knox converted the original TRS-80 code to ANSI-C in 2003. In his conversion, he skipped the map part of the original game. We will get to that when we take a look at the implementation.
Let's dive into the code
Note: If you are interested in the original TRS-80 listing, you will find it here: SoftSide Magazine Issue 03 (Santa Parvia)
The converted code can be found at GitHub.
Disclaimer: I'm no mathematician. I'm a programmer. As such, if I try to describe algorithms or equations used in this game, I'll do that using the terminology of a programmer. There is no point in using a language I do not speak.
Let's unpack the main Run() method of the C# conversion.
The game relies in large parts on randomization. As such, it is no surprise that we need to initialize random functionality. From line 3 ‐ 12, the game will setup the randomizer, greet the player and ask, if the player needs further instructions. If confirmed, the game calls PrintInstructions()
.
A computer back in the days was a highlight to everyone. They were rather expensive, so there was usually one computer for the entire family. As such, it is no wonder that you are able to play this game at the same terminal with more than 1 player. Lines 14 ‐ 21 handle the logic to request the amount of players that are currently in this session.
Lines 23 ‐ 38 set up the difficulty level for this game. It offers four level of difficulty. The numeric value of difficulty is used in the progression calculation of the game, which we will see more of later on.
Depending on the amount of players, lines 40 ‐ 56 loop several times and setup the list of players by asking for names and initializing the player. InitializePlayer()
is used to set several variables to its' initial state. That are not all variables that belong to the player
record, but it's already an impressive list:
Random(35);
is another function that is used rather often in this game. It will provide a random integer between 0 and the supplied parameters, exclusively. In this case, we will receive an integer between 0 and 34.
The game also predetermines your year of death by using the start year (that is always 1400), adding 20 to it (so you will at least play 20 rounds), and then randomly give you the aforementioned 0 to 34 more extra rounds. Of course, you might be defeated by other events, but at some point, you will lose because you died.
Back to the original code. The only thing that is left to do is to call PlayGame()
:
In this method, the computer opponent is introduced, lines 8 and 9 initialize a Baron called "Peppone".
Lines 11 ‐ 39 are running the main loop as long as at least one player is not dead, or none of the players has won the game. That loop triggers the NewTurn()
method for each player in the game. The Baron never participates in these turns. It's only job in this game is to be the attacker if only one player is playing the game.
Finally, we are able to dive into the nooks and crannies of this game. Each turn, the game will run the following steps:
GenerateHarvest()
: Calculate the harvest, the amount of rats and the grain reserve. Harvest and rats are randomized numbers.NewLandAndGrainPrices()
: Calculate the land prices, the grain reserve, the demand for grain and how many grain got eaten by the rats.BuySellGrain()
: Display the grain reserve, the grain demand, the price of grain, price of land and the amount of money owned to the player. Also, ask the player what they would like to do next: Buy land or grain, sell land or grain or move to the next step.ReleaseGrain()
: The player is asked how much grain they want to release to the serfs. The player must keep at least a reserve of 20%, and they have to give out at least 20% of the whole reserve.- The loop from lines 8 ‐ 28 deals with the attack logic. The player object contains a flag,
InvadeMe
, which signals that the player is too weak to defend against an opponent, which will trigger the attack logic. This is where the baron comes in play, if you are a single player. Line 25 will launch the attack with the baron, if there is only one player. AdjustTax()
: As a player, you can adjust the customs duty, the sales tax, the wealth tax and the amount of justice. In this method, the game also calculates the revenue the player made and will seize any assets, if the player is bankrupt.DrawMap()
: In the original TRS-80 code, this draws a neat map that shows you how many buildings you have, and how much land you have in comparison to serfs.StatePurchases()
: Nowadays, one would refer to it as the buy screen. Here, you can buy a marketplace, a woolen mill, a palace (partially), a cathedral (partially) and convert serfs to soldiers.CheckNewTitle()
: This is the progression logic. Here, the game calculates the progression based on multiple values with a cap on each. We'll take a closer look later on.ImDead()
: Yes. It does, what it says. It will print a small in memoriam message, and also randomly pick a cause of death. If you made it past 1450, the only cod will be "of old age after a long reign.", which is kinda neat, to be honest.
The four phases of the game
The original explanation for the game calls out four phases: Harvest Phase, Tax Phase, Map Phase and Public Works Phase. We will dive into each of the phases, referencing the code we just uncovered, and talk a bit about how the game does what (or what I think the game does - I might not have fully understood everything).
Harvest Phase
The functions involved in this phase are the following:
GenerateHarvest()
NewLandAndGrainPrices()
BuySellGrain()
ReleaseGrain()
GenerateHarvest()
The game calculates a random value as harvest, by adding two values. Ones goes from 0 ‐ 4 and the other from 0 ‐ 6 - the result is then divided by 2 in an integer division. Rats are also assigned, randomly, on a scale from 0 ‐ 49.
Finally, the game will calculate the grain reserve by multiplying the existing grain reserve with 100, subtracting the grain reserve multiplied by the amount of rats, and divide everything by 100 again: ((player.GrainReserve * 100) - (player.GrainReserve * player.Rats)) / 100
This means that, if Rats
goes up to 50, your grain reserve will be cut in half, whereas if there are zero rats, your reserve will remain unchanged.

NewLandAndGrainPrices()
This method calculates the grain reserve, the grain demand, land price and what the rats ate. It does that by using two variables, x and y. For this one, I will include the code, so you can follow along with my explanation.
x gets the value of player.Land
, which is the amount of land the player owns.
y is calculated in this way: y = ((player.Serfs - player.Mills) * 100.0) * 5.0;
Afterwards, the method performs a sanity check on the y value: If it becomes less than 0.0, it will be set to 0.0, so y should never be negative. If y is smaller than x, than x will be set to y.
y than receives a new value. It is calculated by player.GrainReserve * 2.0
. Again, another sanity check is performed, with the same result: If y is smaller than x, y will be assigned to x.
That means that, by now, x is either:
- The amount of Land the player owns
- Amount of serfs minus the amount of mills multiplied by 500
- Grain reserve multiplied by 2
After all that, y will be reused:
y = player.Harvest + (myRandom + 0.5);
MyRandom is an offset, calculated in line 7. Basically, it is a random double between 0.0 and 1.0
h = Convert.ToInt32(x * y);
is the final variable needed. So the player's harvest (plus offset) is multiplied by either of the three possibilities mentioned earlier, and then converted to int. As discussed before, harvest will be a value between 0 and 5.
The result of the previous operation is than added to GrainReserve
. This gives us the very first value for this operation, the new grain reserve.
Next, the grain demand is calculated. That operation is pretty straight forward.
The land price calculation spans from 35 to 65. Let's dive a bit deeper into the mechanics:
The initial land price is calculated by multiplying the players harvest (0 - 5) with 3, adding a random number between 0 - 5, and adding 10 to it. This result is then divided by ten. With this formula, the result of the land price calculation varies between 1 and 3.
However, from playing the game, I know that there are land prices below 2, so there is, of course, more happening. The next block will do some operations on h and y. Basically, h will be flipped if it is negative (so it get's positive), and if h is smaller than 1, then y is set to 2. If h is bigger than 2, the grain demand is divided by h. y on the other hand is capped by 2, and by 0.8 on the lower end.
The land price is than further reduced because it get's multiplied by y. If land price goes below 1.0 though, it will be reset to 1.0
Grain price is finally calculated by this formula:
Convert.ToInt32(((6.0 - player.Harvest) * 3.0 + Random(5) + Random(5)) * 4.0 * y);
- again, lot's of random values that get added. From my understanding, the lowest value this formula can output is 9.6, and the highest is 208. That resonates with my experience during the game. While the randomized inputs carry quite some variance in the result, the biggest impact comes from the harvest. And that totally makes sense. The higher the harvest number goes, the lower the price for grain goes (as there is no shortage).
Finally, the value that calculated for h, will also be the fate of your reserve in terms of how much the rats ate.
BuySellGrain()
Not much to see here. This one will allow you to sell or buy either land or grain. It will then go ahead and perform the necessary operations on the resources, and will also add or subtract to or from the treasury.
ReleaseGrain
This one is fun. It will be the most complex function we will take a look at today.
Lines 13 ‐ 38 deal with the fundamental question of how much grain you will distribute for consumption. The game limits you in what you are able to do: You should release at least 20% of your available grain, but you must not release more than 80%. Once these constraints are satisfied, you are allowed to continue.
Lines 40 ‐ 46 reset all necessary variables for this routine on our player class to the initial start values. Line 47 substracts your choosen amount of grain from your reserve.
Our well-known z variable is back again. This time, it represents the ratio between your choosen grain amount and the demand. It then shifts this value by one. This means, that if you satisfied the demand for grain, y will be 0. If you undercut the demand, the value will be negative. If you "overspend", the value will be positive.
Interesting enough: If you overspend, the algorithm will cut y in half (in line 53). If z is then still bigger than 0.25, the game will take this value, divide it by 10 and add 0.25 to it. I'm not quite sure why, but maybe we will figure it out down the line.
In line 61, zp is introduced: It will be added to z in line 75, after being divided by 10. zp starts with an initial value of 50, which is reduced by the customs duty, the sales tax and the player income tax. The taxes have limits:
- Customs Duty: 0 to 100
- Sales Tax: 0 to 50
- Income Tax: 0 to 25
This means, that zp can get as low as -125. If zp goes down to a negative value, it will be multiplied by justice. As a reminder, that's what justice stands for:
- 1: Very fair
- 2: Moderate
- 3: Harsh
- 4: Outrageous
Depending on your justice, zp can go down to as much as -500. However, in line 68, the value is again divided by 10, effectively capping it at -50 to 5. If zp is still bigger than 0, then in line 72, justice is subtracted from 3 and added to the value of zp. Finally, in line 75, zp will again be divided by 10 and added to our previous value of z.
Interesting enough, now z also gets a cap in line 77, which will limit it to 0.5.
Between the lines 80 and 129, the code branches off depending on whether you decided to spend less or more than the demand of grain.
If you happen to spend less than the demand, lines 82 ‐ 96 will calculate the effects of this:
(Convert.ToDouble(player.GrainDemand) - Convert.ToDouble(howMuch)) / player.GrainDemand * 100.0 - 9.0
This code will give you a value of x, which is essentially the percentage of how much your released grain deviates from the demanded grain. If the demand is higher then what you provided, x will be positive. If the demand is lower than what you provided, x will be negative. However, x will be capped to 65. So if the demand is 20000, but you can only supply 2000, x won't go up to 81, it will be capped at 65. If you overspend, this has no effect in this part of the code, as it will simply set x and xp to zero. That is kind of interesting, because this branch only should get entered if the provided amount of grain is already lower than the demand. So x should never get too low.
The next two calls are SerfsProcreating(player, 3.0)
and SerfsDecomposing(player, xp + 8.0)
.
These two function essentially use the same algorithmic logic, of course changing different values in the player class (NewSerfs for SerfsProcreating, and DeadSerfs for SerfsDecomposing). Btw, I did not come up with the name for theses methods.
If you spend more grain than demand was set, the game will start with the calculation of how many serfs were born and how many died. If your customs duty and sales tax combined is less than 35, the game will randomly increase the amount of merchants by 0 to 3. You will also receive nobles and clergies randomly, if your income tax is below a random limit between 0 and 27.
If your surplus of grain released is more than 30%, serfs will start to move into your city. The code will also randomly apply a (reasonably) large amount of merchants, increase the nobles by one and add two clergies into your city. So releasing 130% of the grain in demand kind of pays off:
If your Justice is higher than 2, which equals harsh or outrageous, than there is a punishment called JustiveRevenue. It is calculated by dviding the amount of serfs by 100, multiplied by 16 (only happens if your justice is "outrageous"). The result of this calculation will be the upper limit of a random value, which will be the final JusticeRevenue. This will equal the amount of serfs that are fleeing, and will be subtracted from the amount of serfs.
Next up, the code calculates some revenue streams. Each marketplace generates a revenue of 75, the military has some randomness to it: 55 is the lower limit, 304 is the upper limit of revenue. Soldiers will cost 3 golden florins per soldier.
Finally, the game will check if you will be invaded. However, I think I've spotted an error in the C translation of the original listing.
A small mistake?
The implementation in C does this:
That seems... redundant? So, let's have a look at the original listing:
Mhm. 8100 jumps to the invasion. So - if your ratio of soldiers to land falls under 1 to 1000, than you will be invaded. If your ratio falls down to 1 to 500, you MIGHT get invaded, if another player has at least 2.5 times the amount of soldiers you have. That kind of makes sense.
And indeed, the c version accounts for that:
Our version does the same. However, to circle back to the original question: Yes, that check is redundant, because the differentiation is not done at that place. Which is fine, but that means we can just check for one condition being fulfilled.
And with that, we can wrap up this whole function, as we already dipped our toe into the logic around AttackNeighbor
AttackNeighbor
This function is not too difficult to disassemble. It first checks if the attacker provided is our "Baron". As he does not participate in the game logic, he will randomly take between 1000 and 9999 from you.
Otherwise, the attacker will receive (attacker.soldiers * 1000) - (attacker.Land / 3)
amount of land from the player. Now, if the amount of land taken is bigger than this: opponent.Land - 5000
, than land taken will be changed to (opponent.Land - 5000) / 2
An invasion will also claim the life of your soldiers. How many, you ask? Well, random is your fate. A random number between 0 and 40 will be the amount of dead soldiers. However, your opponent needs to have at least 15 more soldiers than the random amount selected. If that's not the case, the amount of dead soldiers will be Attacker.Soldiers - 15
And with that, the Harvest Phase is concluded. Phew!
Tax Phase
Ah, yes. Taxes. Finally. Time to generate some revenue. This phase consists of the following methods:
- AdjustTax
That's correct: It only has one function.
AdjustTax
This function consists of several subcalls to other functions, but in its' heart, it allows you enter new values for your taxes and your justice, and gives you an overview about the current taxes:
GenerateIncome
This function is the first of many subroutines, and will on the one hand print out the amount of justice in a human readable fashion, and on the other hand calculate all tax and justice related revenue streams.
JusticeRevenue is calculated by multiplying Justice with 300 and subtracting 500 from that value. The result of that operation is then multiplied with the title of the player, generating more income the higher the rank of the player becomes. That also means that a higher justice value will generate more income at the price of a random amount of people fleeing your city at some point.
CustomsDutyRevenue, SalesTaxRevenue and IncomeTaxRevenue all contain a common factor in the calculations, y. This factor is calculated by subtracting the sales tax, customs duty and income tax from 150, and then dividing that value by 100. If y turns below 1.0, the value is locked to 1.0. It's then divided by 100
In essence, this means for the customs duty revenue, that high taxes will negate the effect merchants have on the revenue stream. For sales tax, that eliminates the effect justice has, and for income tax, it eliminates the effect justice and nobles have on the revenue. That is, because y is a float value, but get's casted to integer, thus eliminating the fraction and be either 1 or 0.
AddRevenue
This functions adds the revenue streams that we calculated in the previous method to your treasury. Now, it is possible to spend more money than you own, and even your taxes revenue might not add enough money to the treasury to turn your account positive. If your treasury is negative, the game will penalize you by subtracting another 50% of your deficit from your treasury. Your rank dictates, how much deficit is allowed, by multiplying your rank with -10000. If your treasury is below that value, the game will set the IsBankrupt flag on you.
SeizeAssets
If you are bankrupt, the game will seize your assets. That's pretty straight forward, as the game will basically reset all your possessions.
However, that also means that your game won't end with bankruptcy. Not immediately, that is.
Map Phase
This one is currently not implemented in the c code, and as such, I haven't implemented it yet in my version. Essentially, this will draw a map of your kingdom. You will see the amount of marketplaces, cathedrals and such that you have in your possessions. Later implementations of the game featured more elaborate maps, containing only one of each buildings (mill, cathedral, palace, marketplace), and adding more and more extensions with each level.
The map also displays a serf that moves north towards the wall of your town. If this serf is hitting that wall, it is time for you to buy new land, signalling that there are more people in your town than land is available.
Public Works Phase
In this phase, the game will allow you to buy new assets for your town, or recruit soldiers. The code is not that interesting, so I decided to use a table to lay down the effects.
Asset | Treasury | PublicWorks | Clergy | Nobles | Merchants | Serfs |
---|---|---|---|---|---|---|
Market | -1000 | +1.0 | +5 | |||
Mill | -2000 | +0.25 | ||||
Palace | -3000 | +0.5 | Random(2) | |||
Cathedral | -5000 | +1.0 | Random(6) | |||
Soldiers | -500 | -20a |
a: 20 serfs will be converted to 20 soldiers.
This concludes all four phases of the game.
Progress calculation
The game will calculate your progression on a large amount of factors. Two functions are important for that:
The game will limit each step from line 11 to 22 by ten, using different denominators for each value. In line 23, the difficulty that got selected at the beginning of the game is used as a divisor for the result of this operation. A high justice value will furthermore reduce the title progression.
Final steps
The next steps will conclude the current turn. The year will be increased, and compared against the predetermined player's death year. If the year has come, the ImDead
function will set the ImDead
flag and print a small obituary. If the player has reached the highest title number, the IWon
flag is set. In both cases, in a single player game, the game ends.
Final thoughts
That was more to unpack than I anticipated in the beginning. But it was kind of fun, anyways. The game is surprisingly complex, if you consider it being almost 50 years old (in 2025, at the time of writing). So, what should we do from here on? Well, on the one hand, there is a lot of things to add. You could modify the code to finally print a map. You could go ahead and tweak the variables, remove some kind of randomness and add some more layers of simulation. You could also go ahead and implement a version that uses graphics. The options are endless. I, for my part, am thinking about building a game like this set in the age and time of Cyberpunk.
Make sure to check out my conversion of the game over on Github, and find the downloadable versions in the Release Section.