Evolution Algorithm
How a house decides to evolve, stay the same, or devolve on each monthly tick.
e_house_progress
Every call to check_requirements() returns one of three values:
enum e_house_progress {
HOUSE_DECAY = -1, // requirements not met — devolve
HOUSE_NONE = 0, // all requirements met but evolve threshold not reached
HOUSE_EVOLVE = 1, // all requirements met and evolve threshold reached
};
evolve() Flow
Called once per house per monthly update, dispatched from city::house_process_evolve(). The virtual evolve() in each tier class follows this pattern:
void building_house_ordinary_cottage::evolve(house_demands* demands) {
// 1. Guard: no population → nothing to do
if (base.house_population == 0) return;
// 2. Try to merge 1×1 plots into a 2×2 block
// (only applicable to tiers 1–10)
if (merge()) return; // if merge happened, stop — type already changed
// 3. Check requirements and get evolution signal
e_house_progress result = check_requirements(demands);
// 4. Apply devolve delay (anti-thrash protection)
if (result == HOUSE_DECAY) {
if (!has_devolve_delay()) return; // wait for delay to expire
}
// 5. Apply the transition
switch (result) {
case HOUSE_EVOLVE:
change_to(BUILDING_HOUSE_MODEST_HOMESTEAD);
break;
case HOUSE_DECAY:
change_to(BUILDING_HOUSE_ROUGH_COTTAGE);
break;
case HOUSE_NONE:
break; // no change
}
}
check_requirements()
Compares the house's current state against the model_house_t thresholds for the next tier (for evolve) and the current tier (for devolve).
Desirability check
int desirability = map_desirability_get(base.tile);
if (desirability < model.devolve_desirability[difficulty])
return HOUSE_DECAY;
if (desirability < model.evolve_desirability[difficulty])
return HOUSE_NONE; // desirability met for staying, but not evolving
Services & goods check
has_required_goods_and_services(for_upgrade, demands) validates in order:
| Check | Field tested | Threshold source |
|---|---|---|
| Water | base.has_water_access / has_well_access | Tier-specific: tier 5+ requires Water Supply, not just Well. |
| Food variety | runtime_data.foods[] non-empty count | model.food_types[difficulty] |
| Entertainment | runtime_data.entertainment | model.entertainment[difficulty] |
| Education | runtime_data.education | model.education[difficulty] (0–3) |
| Religion | runtime_data.num_gods | model.religion[difficulty] (0–5) |
| Health | runtime_data.health | model.health[difficulty] (0–2) |
| Dentist | runtime_data.dentist | model.dentist[difficulty] (bool) |
| Physician | runtime_data.physician | model.physician[difficulty] (bool) |
| Pottery | runtime_data.inventory[GOOD_POTTERY] | model.pottery[difficulty] |
| Beer | runtime_data.inventory[GOOD_BEER] | model.beer[difficulty] |
| Linen | runtime_data.inventory[GOOD_LINEN] | model.linen[difficulty] |
| Jewelry | runtime_data.inventory[GOOD_JEWELRY] | model.jewelry[difficulty] |
Any unmet requirement adds a demand entry to the house_demands* accumulator. The city advisor UI reads this accumulator to display the "your people need more schools / temples / ..." messages.
If all checks pass: returns HOUSE_EVOLVE.
If any check fails: returns HOUSE_DECAY and records the demand.
Devolve Delay
To prevent a single missed service walker from instantly downgrading a house, devolution is buffered by a configurable delay counter:
// runtime_data.devolve_delay counts down from model.devolve_delay
// (typically 2–6 ticks depending on tier)
bool building_house::has_devolve_delay() {
if (runtime_data.devolve_delay > 0) {
runtime_data.devolve_delay--;
return false; // not yet — wait for countdown
}
runtime_data.devolve_delay = model.devolve_delay;
return true; // delay expired — apply devolution
}
When requirements are met again before the delay expires the counter resets, and no devolution occurs. This means a house can tolerate brief service gaps (e.g. a walker taking a long path) without degrading.
Size Transitions
Housing footprint grows at specific tier boundaries. This is separate from the tier number — footprint changes require their own transition methods.
| Transition | Tiers involved | Method |
|---|---|---|
| 1×1 → 2×2 merge | Tiers 1–10 (any tier below Residence) | merge() — requires 4 adjacent identical-tier 1×1 plots |
| 2×2 → 3×3 expand | Tier 14 Fancy Residence → Tier 15 Common Manor | expand_to_manor() |
| 3×3 → 4×4 expand | Tier 18 Stately Manor → Tier 19 Modest Estate | expand_to_estate() |
merge() scans the three adjacent tiles (right, bottom, bottom-right). All four plots must be the same building type and all unoccupied by any non-house structure. When merge succeeds the call returns immediately — the house type has already changed and the current evolve() call should not proceed further.
Devolution & Split
When a large house devolves it cannot simply move one step down the tier list — it must also shrink its footprint. Two dedicated devolution paths handle this:
// Manor (3×3) devolution — called from tier 15–18 evolve() on HOUSE_DECAY
void building_house::devolve_to_fancy_residence() {
// split the 3×3 footprint into multiple 2×2 Fancy Residence blocks
split(BUILDING_HOUSE_FANCY_RESIDENCE);
}
// Estate (4×4) devolution — called from tier 19–20 evolve() on HOUSE_DECAY
void building_house::devolve_to_stately_manor() {
// split the 4×4 footprint into multiple 3×3 Stately Manor blocks
split(BUILDING_HOUSE_STATELY_MANOR);
}
split(new_type) places new building instances at the sub-tile positions the original footprint occupied and removes the original. Population is redistributed proportionally across the new buildings.
For 2×2 houses devolving back to 1×1, the merge is simply undone: the four 1×1 Crude Hut plots are restored individually in place of the merged block.