Skip to content

Commit 4cc3f5c

Browse files
committed
pop levels pt1
1 parent f5d9b7c commit 4cc3f5c

File tree

7 files changed

+343
-128
lines changed

7 files changed

+343
-128
lines changed

src/SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
- [Towns](./towns.md)
2727
- [Ticks](./towns/ticks.md)
2828
- [Population](./towns/population.md)
29+
- [Consumption](./towns/population/consumption.md)
30+
- [Satisfaction](./towns/population/satisfaction.md)
31+
- [Levels](./towns/population/levels.md)
2932
- [Ware Prices](./towns/ware-prices.md)
3033
- [Base Price](./towns/ware-prices/base-price.md)
3134
- [Thresholds](./towns/ware-prices/thresholds.md)

src/towns/population.md

Lines changed: 2 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,3 @@
11
# Population
2-
3-
## Consumption
4-
The `do_population_consumption` function is at `0x00527D40`.
5-
6-
At `0x00672860` there is a table that contains the daily consumptions for 100 citizens of every population type:
7-
8-
|Ware|Rich|Wealthy|Poor|Beggars|
9-
|-|-|-|-|-|
10-
|Grain|90|120|150|120|
11-
|Meat|110|87|12|5|
12-
|Fish|40|80|100|110|
13-
|Beer|65|130|65|75|
14-
|Salt|1|1|1|1|
15-
|Honey|50|25|5|2|
16-
|Spices|4|2|2|0|
17-
|Wine|150|38|0|0|
18-
|Cloth|50|35|15|1|
19-
|Skins|60|30|0|0|
20-
|WhaleOil|50|35|10|0|
21-
|Timber|80|80|40|20|
22-
|IronGoods|100|75|25|0|
23-
|Leather|44|35|5|0|
24-
|Wool|10|40|20|5|
25-
|Pitch|0|0|0|0|
26-
|PigIron|0|0|0|0|
27-
|Hemp|5|3|2|3|
28-
|Pottery|30|18|12|1|
29-
|Bricks|1|1|0|0|
30-
|Sword|0|0|0|0|
31-
|Bow|0|0|0|0|
32-
|Crossbow|0|0|0|0|
33-
|Carbine|0|0|0|0|
34-
35-
If a town is not under siege and a ware is in oversupply, more of it is consumed.
36-
**TODO clarify**
37-
**TODO pitch consumption (sieged and unsieged), winter/famine/plague modifiers**
38-
39-
## Satisfaction
40-
P3's setting "Needs of the citizens" changes how easy it is to increase the satisfaction, and how fast it changes.
41-
The *satisfaction classes* are displayed in-game: *Very happy*, *happy*, *very satisfied*, *satisfied*, *dissatisfied*, and *annoyed*.
42-
The satisfaction for each population type is stored in the town's *satisfactions* array at offset `0x300`, holding an `i16` for every population type except Beggars.
43-
The function `prepare_citizens_menu_ui` at `0x0040B570` calculates the satisfaction classes by converting the `i16` into an `f32`, and picking the highest applicable class:
44-
45-
|Satisfaction >|Satisfaction Class|
46-
|-|-|
47-
|29.5|Very Happy|
48-
|19.5|Happy|
49-
|9.5|Very Satisfied|
50-
|0.5|Satisfied|
51-
|-10.5|Dissatisfied|
52-
|-Infinity|Annoyed|
53-
54-
The function `update_citizen_satisfaction` at `0x0051C830` calculates the *current satisfaction* each population type would have.
55-
The satisfaction is then increased or decreaseed by the respective *step size*, depending on whether it was bigger or smaller than the current satisfaction, but it won't go beneath -40 or above 80.
56-
At `0x006736AC` there is a table that contains for every difficulty the step sizes for increments and decrements to the satisfaction:
57-
58-
|Needs Setting|Increment|Decrement|
59-
|-|-|-|
60-
|Low|3|1|
61-
|Normal|2|1|
62-
|High|1|2|
63-
|Unused|1|1|
64-
65-
At `0x006736A0` there is a table that contains for every difficulty the *base satisfaction* for every population type:
66-
67-
|Needs Setting|Rich|Wealthy|Poor|
68-
|-|-|-|-|
69-
|Low|-7|-12|-20|
70-
|Normal|-13|-18|-27|
71-
|High|-20|-25|-32|
72-
73-
Within `update_citizen_satisfaction` 6 *situational modifiers* are implemented:
74-
75-
|Situation|Impact|
76-
|-|-|
77-
|Siege|-10|
78-
|Pirate Attack|-8|
79-
|Plague|-10|
80-
|Blocked|-6|
81-
|Boycotted|-4|
82-
|Famine|-10|
83-
84-
At `0x00672938` there is a table that defines *ware satisfaction weights* for every population type:
85-
86-
|Ware|Rich|Wealthy|Poor|
87-
|-|-|-|-|
88-
|Grain|2|4|8|
89-
|Meat|5|4|4|
90-
|Fish|2|6|6|
91-
|Beer|2|6|6|
92-
|Salt|2|2|4|
93-
|Honey|3|2|0|
94-
|Spices|3|0|0|
95-
|Wine|5|2|0|
96-
|Cloth|5|4|0|
97-
|Skins|3|2|0|
98-
|WhaleOil|3|4|4|
99-
|Timber|3|4|6|
100-
|IronGoods|2|2|0|
101-
|Leather|2|2|4|
102-
|Wool|2|6|4|
103-
|Pitch|0|0|0|
104-
|PigIron|0|0|0|
105-
|Hemp|0|0|0|
106-
|Pottery|3|2|4|
107-
|Bricks|0|0|0|
108-
|Sword|0|0|0|
109-
|Bow|0|0|0|
110-
|Crossbow|0|0|0|
111-
|Carbine|0|0|0|
112-
113-
The current satisfaction is calculated as follows:
114-
```
115-
def get_ware_satisfaction(ware_id, population_type):
116-
if wares[ware_id] >= 2 * weekly_consumption[ware_id]:
117-
return satisfaction_weights[population_type][ware_id]
118-
else:
119-
return (wares[ware_id] - weekly_consumption[ware_id])
120-
* satisfaction_weights[population_type][ware_id]
121-
// weekly_consumption[ware_id]
122-
123-
current_satisfaction = 2 * (
124-
base_satisfaction
125-
+ situational_modifiers
126-
+ unknown_modifiers # 9 total, 8 capped at 4
127-
+ ware_satisfactions
128-
)
129-
```
2+
All things related to the population are handled during the town's tick call.
3+
The following subsections explain the identified functions.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Consumption
2+
The `do_population_consumption` function is at `0x00527D40`.
3+
4+
At `0x00672860` there is a table that contains the daily consumptions for 100 citizens of every population type:
5+
6+
|Ware|Rich|Wealthy|Poor|Beggars|
7+
|-|-|-|-|-|
8+
|Grain|90|120|150|120|
9+
|Meat|110|87|12|5|
10+
|Fish|40|80|100|110|
11+
|Beer|65|130|65|75|
12+
|Salt|1|1|1|1|
13+
|Honey|50|25|5|2|
14+
|Spices|4|2|2|0|
15+
|Wine|150|38|0|0|
16+
|Cloth|50|35|15|1|
17+
|Skins|60|30|0|0|
18+
|WhaleOil|50|35|10|0|
19+
|Timber|80|80|40|20|
20+
|IronGoods|100|75|25|0|
21+
|Leather|44|35|5|0|
22+
|Wool|10|40|20|5|
23+
|Pitch|0|0|0|0|
24+
|PigIron|0|0|0|0|
25+
|Hemp|5|3|2|3|
26+
|Pottery|30|18|12|1|
27+
|Bricks|1|1|0|0|
28+
|Sword|0|0|0|0|
29+
|Bow|0|0|0|0|
30+
|Crossbow|0|0|0|0|
31+
|Carbine|0|0|0|0|
32+
33+
If a town is not under siege and a ware is in oversupply, more of it is consumed.
34+
**TODO clarify**
35+
**TODO pitch consumption (sieged and unsieged), winter/famine/plague modifiers**

src/towns/population/levels.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Levels
2+
The `town_update_population_levels` function at `0x0051C650` determines how many citizens are promoted and demoted and how many poor citizens emigrate.
3+
4+
The following pseudocode denotes the calculation:
5+
```python
6+
class Town:
7+
citizens: list[int]
8+
satisfactions: list[int]
9+
has_mint: bool
10+
dwellings_capacity: list[int]
11+
12+
def __init__(
13+
self,
14+
rich: int,
15+
wealthy: int,
16+
poor: int,
17+
satifsaction_rich: int,
18+
satisfaction_wealthy: int,
19+
satisfaction_poor: int,
20+
has_mint: bool = False,
21+
dwellings_capacity_rich: int = 999999,
22+
dwellings_capacity_wealthy: int = 999999,
23+
dwellings_capacity_poor: int = 999999,
24+
):
25+
self.citizens = [rich, wealthy, poor]
26+
self.satisfactions = [
27+
satifsaction_rich,
28+
satisfaction_wealthy,
29+
satisfaction_poor,
30+
]
31+
self.has_mint = has_mint
32+
self.dwellings_capacity = [
33+
dwellings_capacity_rich,
34+
dwellings_capacity_wealthy,
35+
dwellings_capacity_poor,
36+
]
37+
38+
def update_population_levels(self):
39+
self.update_population_level(0)
40+
self.update_population_level(1)
41+
# TODO poor emigration
42+
43+
def update_population_level(self, level: int):
44+
target = (
45+
self.citizens[2]
46+
* (self.satisfactions[level] + 40)
47+
// self.get_divisor(level)
48+
)
49+
target = min(max(target, 1), self.dwellings_capacity[level])
50+
LOGGER.debug(f"{level} target: {target} stock: {self.citizens[level]}")
51+
if target < self.citizens[level]:
52+
# Current stock exceeds target
53+
demoted = 2 * self.citizens[level] / target + 1
54+
if demoted > self.citizens[level]:
55+
demoted = self.citizens[level] - 1
56+
self.citizens[2] += demoted
57+
self.citizens[level] -= demoted
58+
else:
59+
# Target exceeds current stock
60+
if self.citizens[level]:
61+
promoted = 2 * target // self.citizens[level] + 1
62+
else:
63+
promoted = 1 # Avoid division by zero
64+
if self.citizens[2] > promoted:
65+
LOGGER.debug(f"promoting {promoted} poors to {level}")
66+
self.citizens[2] -= promoted
67+
self.citizens[level] += promoted
68+
69+
def get_divisor(self, level):
70+
if level == 0:
71+
return 213 if self.has_mint else 320
72+
elif level == 1:
73+
return 160
74+
raise Exception()
75+
```
76+
77+
For a fixed number of total inhabitants and satisfactions, the groups converge:
78+
![image](levels1.png)

src/towns/population/levels.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import logging
2+
from matplotlib import pyplot as plt
3+
from matplotlib.ticker import PercentFormatter
4+
import numpy as np
5+
import math
6+
import pytest
7+
8+
LOGGER = logging.getLogger()
9+
logging.basicConfig()
10+
logging.getLogger().setLevel(logging.DEBUG)
11+
12+
13+
class Town:
14+
citizens: list[int]
15+
satisfactions: list[int]
16+
has_mint: bool
17+
dwellings_capacity: list[int]
18+
19+
def __init__(
20+
self,
21+
rich: int,
22+
wealthy: int,
23+
poor: int,
24+
satifsaction_rich: int,
25+
satisfaction_wealthy: int,
26+
satisfaction_poor: int,
27+
has_mint: bool = False,
28+
dwellings_capacity_rich: int = 999999,
29+
dwellings_capacity_wealthy: int = 999999,
30+
dwellings_capacity_poor: int = 999999,
31+
):
32+
self.citizens = [rich, wealthy, poor]
33+
self.satisfactions = [
34+
satifsaction_rich,
35+
satisfaction_wealthy,
36+
satisfaction_poor,
37+
]
38+
self.has_mint = has_mint
39+
self.dwellings_capacity = [
40+
dwellings_capacity_rich,
41+
dwellings_capacity_wealthy,
42+
dwellings_capacity_poor,
43+
]
44+
45+
def get_divisor(self, level):
46+
if level == 0:
47+
return 213 if self.has_mint else 320
48+
elif level == 1:
49+
return 160
50+
raise Exception()
51+
52+
def update_population_levels(self):
53+
self.update_population_level(0)
54+
self.update_population_level(1)
55+
56+
def update_population_level(self, level: int):
57+
target = (
58+
self.citizens[2]
59+
* (self.satisfactions[level] + 40)
60+
// self.get_divisor(level)
61+
)
62+
target = min(max(target, 1), self.dwellings_capacity[level])
63+
LOGGER.debug(f"{level} target: {target} stock: {self.citizens[level]}")
64+
if target < self.citizens[level]:
65+
# Current stock exceeds target
66+
demoted = 2 * self.citizens[level] / target + 1
67+
if demoted > self.citizens[level]:
68+
demoted = self.citizens[level] - 1
69+
self.citizens[2] += demoted
70+
self.citizens[level] -= demoted
71+
else:
72+
# Target exceeds current stock
73+
if self.citizens[level]:
74+
promoted = 2 * target // self.citizens[level] + 1
75+
else:
76+
promoted = 1 # Avoid division by zero
77+
if self.citizens[2] > promoted:
78+
LOGGER.debug(f"promoting {promoted} poors to {level}")
79+
self.citizens[2] -= promoted
80+
self.citizens[level] += promoted
81+
82+
83+
def plot_example1():
84+
plt.clf()
85+
labels = ["Rich", "Wealthy", "Poor"]
86+
fig, (axs) = plt.subplots(1, 4, sharey="all", sharex="all", figsize=(10, 4))
87+
plot_example1_subplot(axs[0], 5, False)
88+
plot_example1_subplot(axs[1], 5, True)
89+
plot_example1_subplot(axs[2], 80, False)
90+
plot_example1_subplot(axs[3], 80, True)
91+
92+
ax2 = axs[-1].twinx()
93+
ax2.set_ylim(*axs[-1].get_ylim())
94+
ax2.yaxis.set_major_formatter(PercentFormatter(1000))
95+
96+
fig.suptitle("Convergence of 1000 Inhabitants")
97+
fig.legend(
98+
labels,
99+
bbox_to_anchor=(1, 1),
100+
loc="upper left",
101+
# bbox_transform=plt.gcf().transFigure,
102+
)
103+
plt.tight_layout()
104+
plt.savefig("levels1.png", dpi=100, bbox_inches="tight")
105+
106+
107+
def plot_example1_subplot(ax, satisfaction: int, has_mint: bool):
108+
ax.grid(axis="y")
109+
town = Town(50, 50, 900, satisfaction, satisfaction, satisfaction, has_mint)
110+
x = []
111+
sat_y_rich = []
112+
sat_y_wealthy = []
113+
sat_y_poor = []
114+
for i in range(0, 10):
115+
town.update_population_levels()
116+
for i in range(0, 60):
117+
x.append(i)
118+
sat_y_poor.append(town.citizens[2])
119+
sat_y_wealthy.append(town.citizens[1])
120+
sat_y_rich.append(town.citizens[0])
121+
town.update_population_levels()
122+
123+
ax.plot(x, sat_y_rich, label="Rich")
124+
ax.plot(x, sat_y_wealthy, label="Wealthy")
125+
ax.plot(x, sat_y_poor, label="Poor")
126+
ax.set_title(f"{satisfaction} Satisfaction {"(Mint)" if has_mint else "(no Mint)"}")
127+
128+
129+
def plot_example2():
130+
pass
131+
132+
133+
plot_example1()
134+
plot_example2()

src/towns/population/levels1.png

59.5 KB
Loading

0 commit comments

Comments
 (0)