Wiki / Developer Docs / Housing System / Evolution Algorithm

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:

CheckField testedThreshold source
Waterbase.has_water_access / has_well_accessTier-specific: tier 5+ requires Water Supply, not just Well.
Food varietyruntime_data.foods[] non-empty countmodel.food_types[difficulty]
Entertainmentruntime_data.entertainmentmodel.entertainment[difficulty]
Educationruntime_data.educationmodel.education[difficulty] (0–3)
Religionruntime_data.num_godsmodel.religion[difficulty] (0–5)
Healthruntime_data.healthmodel.health[difficulty] (0–2)
Dentistruntime_data.dentistmodel.dentist[difficulty] (bool)
Physicianruntime_data.physicianmodel.physician[difficulty] (bool)
Potteryruntime_data.inventory[GOOD_POTTERY]model.pottery[difficulty]
Beerruntime_data.inventory[GOOD_BEER]model.beer[difficulty]
Linenruntime_data.inventory[GOOD_LINEN]model.linen[difficulty]
Jewelryruntime_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.

TransitionTiers involvedMethod
1×1 → 2×2 mergeTiers 1–10 (any tier below Residence)merge() — requires 4 adjacent identical-tier 1×1 plots
2×2 → 3×3 expandTier 14 Fancy Residence → Tier 15 Common Manorexpand_to_manor()
3×3 → 4×4 expandTier 18 Stately Manor → Tier 19 Modest Estateexpand_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.